Comme le dit le site Danga qui maintient le projet
memcached is a high-performance, distributed memory object caching system, generic in nature, but intended for use in speeding up dynamic web applications by alleviating database load.
On peut faire l’analogie avec les IPC sous Un*x : mémoire partagée et gestion des sémaphores, mais memcached permet le partage de données entre plusieurs machines ou processus à partir d’un cache serveur(s).
Memcached est utilisé par de gros sites tels que Live Journal, Facebook, Digg ou Slashdot. Le plus de memcached est qu’il est fournit avec de nombreuses API : Perl, .NET, Php, Ruby, …, ce qui facilite l’interopérabilité entre les systèmes.
- Installation sous Debian : apt-get install memcached
- Fichier de configuration : /etc/memcached.conf
Quelques ressources sur memcached
Dans le cadre d’un projet, j’en ai eu besoin afin de partager des URLs1 entre un serveur Linux et un serveur Windows 2003, ces 2 derniers effectuant des traitements sur des mails. Le service memcached2 étant installé sur le serveur Debian. L’affichage du résultat des 2 traitements Linux/Windows s’effectue sur une simple page ASP.NET d’un 3ème serveur.
memcached pourrait remplacer tout système de cache pour le Web (l’objet Cache d’ASP.NET compris). Le cache est primordial pour tout site un tant soit peu sérieux. 2 implémentations de la librairie C# pour le client memcached : Memcacheddotnet, EnyimMemcached (non testé).
Ci-après le code des 2 scripts à titre d’exemples, ils ne sont pas opérationnels tels que.
Script Perl
Dans le script Perl, les méthodes à connaître :
- le constructeur, le cache peut être géré par plusieurs serveur de façon transparent : my $memd = new Cache::Memcached { ‘servers’ => [ “localhost:11211” ], };
- stocker une valeur, $expire (expiration de la donnée en sec.) est optionnel : $memd->set($cle,$valeur,$expire);
- obtenir une valeur : $val = $memd->get(“macle”);
Code complet du script Perl de traitement. Il a pour but de traiter des mails (bounces) provenant d’une douane anti-spams afin d’en extraire les URLs d’accès à l’interface Web de la quarantaine pour le mail concerné.
#!/usr/bin/perl
use lib '/home/sympa/bin';
use List;
use Conf;
use Log;
use Message;
use DBI;
use MIME::Parser;
use Cache::Memcached;
# email de notifications de la douane anti-spam
my $fromdouane="no-reply\\@xxxx.net";
my $regexp = "^(http://.+)";
my $expire = 8*60*60;
&Conf::load('/etc/sympa.conf') || die 'config_error';
my $spool = $Conf{'queue'};
my $memd = new Cache::Memcached { 'servers' => [ "localhost:11211" ], };
# traitement d'un message
# sortir l'URL d'accès à la quarantaine de la douane
# pour le mail concerné
sub TraiteMessage {
($message) = @_;
my $msg = $message->{'msg'};
my $hdr = $msg->head;
my $rcpt = $message->{'rcpt'};
my $sender = $message->{'sender'};
($listname, $robot) = split(/\\@/,$rcpt);
$listname = lc($listname);
if ($msg->is_multipart()) {
my $status = &tools::as_singlepart($msg, 'text/plain');
unless (defined $status) {
&do_log('err', 'Could not change multipart to singlepart');
return undef;
}
}
if (defined $msg->bodyhandle) {
my $mime = $hdr->get('Mime-Version') ;
my $content_type = $hdr->get('Content-type');
my $transfert_encoding = $hdr->get('Content-transfer-encoding');
unless (($content_type =~ /text/i and !$mime)
or !($content_type)
or ($content_type =~ /text\\/plain/i)) {
return $success;
}
my @body = $msg->bodyhandle->as_lines();
foreach $i (@body) {
#if ($transfert_encoding =~ /quoted-printable/i) {
# $i = MIME::QuotedPrint::decode($i);
#}
$i =~ s/^\\s*>?\\s*(.*)\\s*$/$1/g;
next if ($i =~ /^$/); ## skip empty lines
next if ($i =~ /^\\s*\\#/) ;
if( $i =~ /^$regexp/m ) {
$memd->set($listname,$i,$expire);
}
}
}
}
sub TraiteSpams {
my ($spool_dir) = @_;
unless (opendir(DIR, $spool_dir)) {
return undef;
}
my @qfile = sort grep (!/^\\.+$/,readdir(DIR));
closedir DIR;
foreach my $f (sort @qfile) {
my $message = new Message("$spool_dir/$f");
my $sender = $message->{'sender'};
if(defined $sender && "$sender" eq "$fromdouane") {
TraiteMessage($message);
}
}
}
&TraiteSpams("$spool/bad");
code de la classe C#
Code complet de la classe C# pour le traitement :
using Memcached.ClientLibrary;
using LumiSoft.Net.Mime;
public class DetectSpams : ITask
{
SockIOPool pool = null;
MemcachedClient mc = null;
Regex regex = new Regex("(http://.+[^\ ])",
RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase);
DataTable dt;
DBContext CurrentContext;
string fromdouane = CcinetConfig.Application["emailDouane"];
bool debug = CcinetConfig.Application["debug"] == "true";
string sqldouane = "";
/// <summary>
/// Init DB Lyris
/// </summary>
private void initDb()
{
CurrentContext = new DBContext();
CurrentContext.InitConfig(CcinetConfig.Global, "DbConfigLyris");
// extraction de la requête à exécuter : retrouve les messages de notifications de quarantaine
XmlDocument xsql = new XmlDocument();
xsql.Load(CcinetConfig.Application["SQLdouane"]);
XmlNode sqln = xsql.SelectSingleNode("//SQLdouane");
sqldouane = string.Format(sqln.InnerText, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, fromdouane);
}
/// <summary>
/// Init memcached
/// </summary>
private void initMemcached()
{
String[] serverlist = CcinetConfig.Application["memcachedServers"].Split(new char[] {';',',',' '});
pool = SockIOPool.GetInstance();
pool.SetServers(serverlist);
pool.InitConnections = 3;
pool.MinConnections = 3;
pool.MaxConnections = 5;
pool.SocketConnectTimeout = 1000;
pool.SocketTimeout = 3000;
// évite la maintenance des threads du pool : pb. pour la tâche lancée dans son propre thread
pool.MaintenanceSleep = 0;
pool.Failover = true;
pool.Nagle = false;
pool.Initialize();
mc = new MemcachedClient();
mc.EnableCompression = false;
mc.PrimitiveAsString = true;
}
private void shutdownMemcached()
{
SockIOPool.GetInstance().Shutdown();
}
private void getUrl(Stream msg)
{
Mime message = Mime.Parse(msg);
// traitement du message pour extraire l'URL puis sotckage dans le cache
if (message.MainEntity.From.Mailboxes.Length > 0 &&
message.MainEntity.From.Mailboxes[0].EmailAddress == fromdouane &&
message.MainEntity.To.Mailboxes.Length > 0)
{
string lstname = message.MainEntity.To.Mailboxes[0].EmailAddress.Split('@')[0];
foreach (MimeEntity mime in message.MimeEntities)
{
/// on prend uniquement le texte contenu dans la partie text/plain
if (mime.ContentType == MediaType_enum.Text_plain)
{
Match m = regex.Match(mime.DataText);
if (m.Success)
mc.Set(lstname, m.Value, DateTime.Now.AddHours(double.Parse(CcinetConfig.Application["expireHours"])));
}
}
}
}
/// <summary>
/// Transforme les \
en \
\ pour respect RFC 2822
/// </summary>
/// <param name="field"></param>
/// <returns></returns>
private string FoldField(string field)
{
string stmp = field.Replace("\
\ ", "\
");
return stmp.Replace("\ ", "\
").Replace("\
", "\
\ ");
}
/// <summary>
/// Stream pour LumiSoft.Mime
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
private Stream getStream(string s)
{
byte[] b = Encoding.Default.GetBytes(s);
MemoryStream ms = new MemoryStream(b);
return (Stream)ms;
}
public void detectSpams()
{
initDb();
initMemcached();
try
{
CurrentContext.AddQueryResult(sqldouane, "NOTIFSSPAMS", true,false,true);
dt = CurrentContext.dataSet.Tables["NOTIFSSPAMS"];
// construction du message entêtes + corps
for (int i = 0; i < dt.Rows.Count; i++)
{
StringBuilder sb = new StringBuilder();
sb.AppendFormat("{0}\
\ \
\ ", FoldField((string)dt.Rows[i]["hdrall_"]));
sb.Append(FoldField((string)dt.Rows[i]["body_"]));
getUrl(getStream(sb.ToString()));
}
}
catch (Exception e)
{
LogManager.WriteLog("WebArcSvc", string.Format("DetectSpams : {0}\ {1}", e.Message, e.StackTrace), LogType.ERROR);
}
shutdownMemcached();
}
#region ITask Members
public void Execute(TaskCallback taskCallback, object[] args)
{
detectSpams();
}
#endregion
}
consommation des données dans le cache
Il reste à afficher ce que je souhaite sur le serveur Web, sur une page de gestion, le code pour afficher les données contenues dans le cache sur un “repeater” :
void AfficheNotifsSpams_PreRender(object sender, EventArgs e)
{
Hashtable ht = new Hashtable();
ListeDiffusionCollection lddcoll = ListeDiffusion.GetAllLists();
for (int i = 0; i < lddcoll.Count; i++)
{
string ldd = ((ListeDiffusion)lddcoll[i]).Libelle.ToLower();
string url = (string) mc.Get(ldd);
if (url != null &&
!ht.ContainsKey(ldd))
ht[ldd] = url;
}
rpNotifs.DataSource = ht;
rpNotifs.DataBind();
}
1 trouvés après des traitements dans des mails par un script Perl sous Debian et un service Windows C#
2 disponible aussi pour windows