时间: 2023-08-15 admin 互联网




    在 Linux 服务器上,通过使用 Nginx 实现负载均衡,或者在阿里云直接购买负载均衡,根据配置的转发规则,不同的请求会被转发到其不同的服务器上进行处理。如果遇到需要上传图片的情况,那最后只有其中一台服务器存有这张图片,而其他服务器则没有。随后,如果请求获取这张图片,但是转发到的恰好是没有存有这张图片的服务器,那么请求就失败了。为了避免这类问题,就需要同步相应的目录上的文件。

Lsyncd 简述:

    Lsyncd 是一个简单高效的文件同步工具,通过lua语言封装了 inotify 和 rsync 工具,采用了 Linux 内核(2.6.13 及以后)里的 inotify 触发机制,然后通过rsync去差异同步,达到实时的效果。


1、安装 lua 语言依赖包

yum install lua
yum install lua-devel

2、安装 Lsyncd 

yum install lsyncd

    这里一定要注意,最好安装最新的版本,笔者安装的版本是 2.2.2 。网上很多教程的版本是2.1.5 ,这个版本有 bug ,但在后续的版本里已经修复,直接安装即可。之后,你可以发现在 etc 目录下,不但多了 lsyncd.conf 配置文件,而且还多了 rsync 工具的配置文件 rsyncd.conf 。这说明 Lsyncd 工具确是使用 rsync 工具创建起来的,通过 rsync 去进行目录的差异同步。


    通过 Lsyncd 工具同步负载均衡转发规则下的服务器,需要在涉及的服务器上都安装好 Lsyncd ,一般通过 SSH 远程登录,进行远程同步。因此,在远端被同步的服务器上开启 SSH 无密码登录,请注意用户身份,将对应的用户 user 公钥 复制到被同步的服务器的 .ssh 文件目录下的 authorized_keys 文件里,最后测试是否可以无密码登陆。

    如果要实现文件的双向同步,那就要对两台服务器进行差不多的 SSH 配置操作,就可以相互进行无密码登陆了。

chmod 600 /user/.ssh/id_rsa
ssh user@


vi /etc/lsyncd.conf
-- User configuration file for lsyncd.
-- Simple example for default rsync, but executing moves through on the target.
-- For more examples, see /usr/share/doc/lsyncd*/examples/
settings{logfile = "/var/log/lsyncd/lsyncd.log",statusFile = "/var/log/lsyncd/lsyncd.status",inotifyMode = "CloseWrite",maxProcesses = 10,nodaemon = false,maxDelays = 7
sync{default.rsync, source = "/data/wwwroot/default/application",target = "user@",init = false,delete = true,delay = 3,rsync = {binary = "/usr/bin/rsync",compress = true,archive = true,verbose = true}
sync{default.rsync,source = "/data/wwwroot/default/public",target = "user@",init = false,delete = true,delay = 3,rsync = {binary = "/usr/bin/rsync",compress = true,archive = true,verbose = true}
lsyncd /etc/lsyncd.conf

    如果要实现文件的双向同步,那就要对两台服务器进行差不多的 Lsyncd 配置操作,下面列出具体需要更改的参数。

    logfile 本地存放 Lsyncd 日志的路径,一般直接使用默认的路径就可以了。

    statusFile 本地存放 状态文件的路径,一般直接使用默认的路径就可以了。

    source 本地源目录路径。

    target 远程目的目录路径,注意这里的SSH远程同步写法。



    运行 Lsyncd 工具后,可以到源目录下创建几个文本,查看是否能成功同步到远程的目的目录下。如果没有成功,可以到 /var/log/lsyncd/lsyncd.log 文件查看详情。

    如果配置了 lsyncd.conf 文件,可以不配置 rsyncd.conf 了。


Linux version 3.10.0-693.11.6.el7.x86_64 ( (gcc version 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC) ) #1 SMP Thu Jan 4 01:06:37 UTC 2018


-- User configuration file for lsyncd.
-- While this example does not do anything it shows
-- how user custom alarms can be now. It will log
-- "Beep!" every 5 seconds.
settings.nodaemon = truelocal function noAction (inlet)-- just discard any events that happes in source dir.inlet.discardEvent(inlet.getEvent())
-- Adds a watch to some not so large directory for this example.
local in1 = sync{source="/usr/local/etc/", action = noAction }local function myAlarm(timestamp, extra)log("Normal", extra.message)spawn(extra.inlet.createBlanketEvent(), "/bin/echo", extra.message)alarm(timestamp + 5, myAlarm, extra)
endalarm(now() + 5, myAlarm, {inlet = in1, message = "Beep"})

-- User configuration file for lsyncd.
-- This example uses local bash commands to keep two local
-- directory trees in sync.
settings {logfile         = "/tmp/lsyncd.log",statusFile      = "/tmp/lsyncd.stat",statusIntervall = 1,nodaemon        = true,
-- for testing purposes. prefix can be used to slow commands down.
-- prefix = "sleep 5 && "
prefix = ""-----
-- for testing purposes. uses bash command to hold local dirs in sync.
bash = {delay = 0,maxProcesses = 1,-- calls `cp -r SOURCE/* TARGET` only when there is something in SOURCE-- otherwise it deletes contents in the target if there.onStartup = [[
if [ "$(ls -A ^source)" ]; thencp -r ^source* ^target;
elseif [ "$(ls -A ^target)" ]; then rm -rf ^target/*; fi
fi]],onCreate = prefix..[[cp -r ^sourcePath ^targetPathdir]],onModify = prefix..[[cp -r ^sourcePath ^targetPathdir]],onDelete = prefix..[[rm -rf ^targetPath]],onMove   = prefix..[[mv ^o.targetPath ^d.targetPath]],
}sync{bash, source="src", target="/path/to/trg/"}

-- User configuration file for lsyncd.
-- This example uses just echos the operations
-- for testing purposes. just echos what is happening.
echo = {maxProcesses = 1,delay = 1,onStartup = "/bin/echo telling about ^source",onAttrib  = "/bin/echo attrib ^pathname",onCreate  = "/bin/echo create ^pathname",onDelete  = "/bin/echo delete ^pathname",onModify  = "/bin/echo modify ^pathname",onMove    = "/bin/echo move ^o.pathname -> ^d.pathname",
}sync{echo, source="src", target="/path/to/trg/"}
-- User configuration file for lsyncd.
--    Syncs with 'lftp'.
--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~lftp = {------- Spawns rsync for a list of events--action = function(inlet)-- gets all events ready for syncinglocal elist = inlet.getEvents(function(event)return event.etype ~= 'Init' and event.etype ~= 'Blanket'end)------- replaces filter rule by literals--local function sub(p)if not p thenreturnendreturn p:gsub('%?', '\\?'):gsub('%*', '\\*'):gsub('%[', '\\['):gsub('%]', '\\]')endlocal config = inlet.getConfig()local commands = elist.getPaths(function(etype, path1, path2)if etype == 'Delete' thenif string.byte(path1, -1) == 47 thenreturn 'rm -r '..config.targetdir..sub(path1)elsereturn 'rm '..config.targetdir..sub(path1)endelseifetype == 'Create' oretype == 'Modify' oretype == 'Attrib'thenif string.byte(path1, -1) == 47 thenreturn 'mirror -R '..config.source..sub(path1)..' '..config.targetdir..sub(path1)elsereturn 'put '..config.source..sub(path1)..' -o '..config.targetdir..sub(path1)endendend)if #commands == 0 thenspawn(elist, '/bin/true')returnendcommands = table.concat(commands, ';\n')log('Normal', 'Calling lftp with commands\n', commands)spawn(elist, '/usr/bin/lftp','<', commands,'-u', config.user..','..config.pass,,------- Spawns the recursive startup sync--init = function(event)local config = event.configlocal inlet = event.inletlocal excludes = inlet.getExcludes()local delete = nilif config.delete then delete = { '--delete', '--ignore-errors' }; endif #excludes ~= 0 thenerror('lftp does not work with excludes', 4)endlog('Normal', 'recursive startup lftp: ', config.source, ' to host: ',, '/usr/bin/lftp','-c','open -u '..config.user..','..config.pass..' ''; '..'mirror -R -e '..config.source..' '..config.targetdir..';')end,------- Checks the configuration.--prepare = function(config)if not thenerror('lftps needs "host" configured', 4);endif not config.user thenerror('lftps needs "user" configured', 4);endif not config.pass thenerror('lftps needs "pass" configured', 4);endif not config.targetdir thenerror('lftp needs "targetdir" configured', 4)endif thenerror('lftp needs NOT "target" configured', 4)endif config.exclude thenerror('lftp does not work with excludes', 4)endif config.rsyncOpts thenerror('lftp needs NOT "rsyncOpts" configured', 4)endif string.sub(config.targetdir, -1) == '/' thenerror('please make targetdir not end with a /', 4)endend,------- Exit codes for rsync.--exitcodes = {[  0] = 'ok',[  1] = 'ok',},------- Default delay--delay = 1,
}sync{lftp,host      = 'localhost',user      = 'test',pass      = 'test',source    = 'src',targetdir = '.',
-- User configuration file for lsyncd.
-- This example refers to one common challenge in multiuser unix systems.
-- You have a shared directory for a set of users and you want
-- to ensure all users have read and write permissions on all
-- files in there. Unfortunally sometimes users mess with their
-- umask, and create files in there that are not read/write/deleteable
-- by others. Usually this involves frequent handfixes by a sysadmin,
-- or a cron job that recursively chmods/chowns the whole directory.
-- This is another approach to use lsyncd to continously fix permissions.
-- One second after a file is created/modified it checks for its permissions
-- and forces group permissions on it.
-- This example regards more the handcraft of bash scripting than lsyncd.
-- An alternative to this would be to load a Lua-Posix library and do the
-- permission changes right within the onAction handlers.----
-- forces this group.
fgroup = "staff"-----
-- script for all changes.
command =
-- checks if the group is the one enforced and sets them if not
perm=`stat -c %A ^sourcePathname`
if [ `stat -c %G ^sourcePathname` != ]]..fgroup..[[ ]; then/bin/chgrp ]]..fgroup..[[ ^sourcePathname || /bin/true;
]] ..-- checks if the group permissions are rw and sets them
if [ `expr match $perm ""` == 0 ]; then/bin/chmod g+rw ^sourcePathname || /bin/true;
]] ..-- and forces the executable bit for directories.
if [ -d ^sourcePathname ]; thenif [ `expr match $perm "......x"` == 0 ]; then/bin/chmod g+x ^^sourcePathname || /bin/true;fi
]]-- on startup recursevily sets all group ownerships
-- all group permissions are set to rw
-- and to executable flag for directories
-- the carret as first char tells Lsycnd to call a shell altough it
-- starts with a slash otherwisw
startup =
[[^/bin/chgrp -R ]]..fgroup..[[ ^source || /bin/true &&
/bin/chmod -R g+rw ^source || /bin/true &&
/usr/bin/find ^source -type d | xargs chmod g+x
]]gforce = {maxProcesses = 99,delay        = 1,onStartup    = startup,onAttrib     = command,onCreate     = command,onModify     = command,-- does nothing on moves, they won't change permissionsonMove       = true,
}sync{gforce, source="/path/to/share"}
-- Lsyncd user-script that creates a "magic" image converter directory.
-- This configuration will automatically convert all images that are placed
-- in the directory 'magicdir' all resulting images are placed in the same
-- directory!
-- Be sure to mkdir 'magicdir' first.-----
-- Fileformats:   .jpg  .gif  .png
local formats = { jpg=true, gif=true, png=true,  }convert = {delay = 0,maxProcesses = 99,action = function(inlet)local event = inlet.getEvent()if event.isdir then-- ignores events on dirsinlet.discardEvent(event)returnend-- extract extension and basefilenamelocal p    = event.pathnamelocal ext  = string.match(p, ".*%.([^.]+)$")local base = string.match(p, "(.*)%.[^.]+$")if not formats[ext] then-- an unknown extenionlog("Normal", "not doing something on ."..ext)inlet.discardEvent(event)returnend-- autoconvert on create and modifyif event.etype == "Create" or event.etype == "Modify" then-- builds one bash commandlocal cmd = ""-- do for all other extensionsfor k, _ in pairs(formats) doif k ~= ext then-- excludes files to be created, so no-- followup actions will occurinlet.addExclude(base..'.'..k)if cmd ~= ""  thencmd = cmd .. " && "endcmd = cmd..'/usr/bin/convert "'..event.source..p..'" "'..event.source..base..'.'..k..'" || /bin/true'endendlog("Normal", "Converting "..p)spawnShell(event, cmd)returnend-- deletes all formats if you delete oneif event.etype == "Delete" then-- builds one bash commandlocal cmd = ""-- do for all other extensionsfor k, _ in pairs(formats) doif k ~= ext then-- excludes files to be created, so no-- followup actions will occurinlet.addExclude(base..'.'..k)if cmd ~= ""  thencmd = cmd .. " && "endcmd = cmd..'rm "'..event.source..base..'.'..k..'" || /bin/true'endendlog("Normal", "Deleting all "..p)spawnShell(event, cmd)returnend-- ignores other events.inlet.discardEvent(event)end,------- Removes excludes when convertions are finished--collect = function(event, exitcode)local p     = event.pathnamelocal ext   = string.match(p, ".*%.([^.]+)$")local base  = string.match(p, "(.*)%.[^.]+$")local inlet = event.inletif event.etype == "Create" orevent.etype == "Modify" orevent.etype == "Delete"thenfor k, _ in pairs(formats) doinlet.rmExclude(base..'.'..k)endendend,------- Does not collapse anythingcollapse = function()return 3end,
}sync{convert, source="magicdir", subdirs=false}
-- User configuration file for lsyncd.
-- This needs lsyncd >= 2.0.3
-- This configuration will execute a command on the remote host
-- after every successfullycompleted rsync operation.
-- for example to restart servlets on the target host or so.local rsyncpostcmd = {-- based on default rsync.default.rsync,checkgauge = {default.rsync.checkgauge,host = true,targetdir = true,target = true,postcmd = true,},-- for this config it is important to keep maxProcesses at 1, so-- the postcmds will only be spawned after the rsync completedmaxProcesses = 1,-- called whenever something is to be doneaction = function(inlet)local event = inlet.getEvent()local config = inlet.getConfig()-- if the event is a blanket event and not the startup,-- its there to spawn the webservice restart at the target.if event.etype == "Blanket" then-- uses rawget to test if "isPostcmd" has been set without-- triggering an error if not.local isPostcmd = rawget(event, "isPostcmd")if isPostcmd thenspawn(event, "/usr/bin/ssh",, config.postcmd)returnelse-- this is the startup, forwards it to default routine.return default.rsync.action(inlet)enderror("this should never be reached")end-- for any other event, a blanket event is created that-- will stack on the queue and do the postcmd when its finishedlocal sync = inlet.createBlanketEvent()sync.isPostcmd = true-- the original event is simply forwarded to the normal action handlerreturn default.rsync.action(inlet)end,-- called when a process exited.-- this can be a rsync command, the startup rsync or the postcmdcollect = function(agent, exitcode)-- for the ssh commands 255 is network error -> try againlocal isPostcmd = rawget(agent, "isPostcmd")if not agent.isList and agent.etype == "Blanket" and isPostcmd thenif exitcode == 255 thenreturn "again"endreturnelse--- everything else, forward to default collection handlerreturn default.collect(agent,exitcode)enderror("this should never be reached")end,-- called before anything else-- builds the target from host and targetdirprepare = function(config, level, skipTarget)if not thenerror("rsyncpostcmd neets 'host' configured", 4)endif not config.targetdir thenerror("rsyncpostcmd needs 'targetdir' configured", 4)endif not = .. ":" .. config.targetdirendreturn default.rsync.prepare(config, level, skipTarget)end
}sync {rsyncpostcmd,source = "src",host = "beetle",targetdir = "/path/to/trg",postcmd = "/usr/local/bin/",
-- User configuration file for lsyncd.
-- Simple example for default rsync.
settings {statusFile = "/tmp/lsyncd.stat",statusInterval = 1,
-- User configuration file for lsyncd.
-- Simple example for default rsync, but executing moves through on the target.
sync{default.rsyncssh, source="src", host="localhost", targetdir="dst/"}
-- An Lsyncd+IRC-Bot Config
-- Logs into an IRC channel and tells there everything that happens in the
-- watched directory tree.
-- The challenge coding Lsyncd configs taking use of TCP sockets is
-- that they must not block! Otherwise Lsyncd will block, no longer
-- empty the kernels monitor queue, no longer collecting zombie processes,
-- no longer spawning processes (this example doesnt do any, but maybe you
-- might want to do that as well), blocking is just bad.
-- This demo codes just minimal IRC functionality.
-- it does not respond to anything else than IRC PING messages.
-- There is no flood control, if a lot happens the IRC server will disconnect
-- the bot.
-- Requires "luasocket" to be installed
require("socket")-- For demo reasons, do not detach
settings.nodaemon = true
hostname = ""
--hostname = ""
port = 6667
nick = "lbot01"
chan = "##lfile01"-- this blocks until the connection is established
-- for once lets say this ok since Lsyncd didnt yet actually
-- start.
local ircSocket, err = socket.connect(hostname, port)
if not ircSocket thenlog("Error", "Cannot connect to IRC: ", err)terminate(-1)
end-- from now on, the socket must not block!
ircSocket:settimeout(0)-- Buffers for stuff to send and receive on IRC:
local ircWBuf = ""
local ircRBuf = ""-- Predeclaration for functions calling each other
local writeIRC-----
-- Called when the IRC socket can be written again.
-- This happens when writeIRC (see below) couldnt write
-- its buffer in one go, call it again so it can continue its task.
local function ircWritey(fd)writeIRC()
-- Called when there is data on the socket
local function ircReady(socket)local l, err, ircRBuf = ircSocket:receive("*l", ircRBuf)if not l thenif err ~= "timeout" thenlog("Error", "IRC connection failed: ", err)terminate(-1)endelseircRBuf = ""endlog("Normal", "ircin :", l)--- answers ping messageslocal ping = l:match("PING :(.*)")if ping thenwriteIRC("PONG :", ping, "\n")end
-- Writes on IRC socket
-- Do not forget to add an "/n".
function writeIRC(...)-- Appends all arbuments into the write bufferircWBuf = ircWBuf..table.concat({...})-- Gives it to the socket and sees how much it acceptedlocal s, err = ircSocket:send(ircWBuf)-- If it cant the socket terminated.if not s and err~="timeout" thenlog("Error", "IRC connection failed: ", err)terminate(-1)end--- logs what has been send, without the linefeed.if (ircWBuf:sub(s, s) == "\n") thenlog("Normal", "ircout:", ircWBuf:sub(1, s - 1))elselog("Normal", "ircout: ", ircWBuf:sub(1, s), "\\")end---- reduces the buffer by the amount of data sent.ircWBuf = ircWBuf:sub(s + 1, -1)-- when the write buffer is empty tell the core to no longer-- call ircWritey if data can be written on the socket. There-- is nothing to be written. If there is data in the buffer-- asks to be called as soon it can be written againif ircWBuf == "" thenobservefd(ircSocket:getfd(), ircReady, nil)elseobservefd(ircSocket:getfd(), ircReady, ircWritey)end
end-- Aquires the nick on IRC and joins the configured channel
-- This will also register the ircReady/ircWritey function at the core
-- to be called when the socket is ready to be read/written.
writeIRC("NICK ", nick, "\n")
writeIRC("USER ", nick, " 0 * :lsyncd-sayirc-bot", "\n")
writeIRC("JOIN ", chan, "\n")-- As action tells on IRC what the action is, then instead of
-- spawning somthing, it discards the event.
local function action(inlet)-- event2 is the target of a move eventlocal event, event2 = inlet.getEvent()if not event2 thenwriteIRC("PRIVMSG ",chan," :",event.etype," ",event.path, "\n")elsewriteIRC("PRIVMSG ",chan," :",event.etype," ",event.path," -> ",event2.path, "\n")endinlet.discardEvent(event)
end-- Watch a directory, and use a second for delay to aggregate events a little.
sync{source = "src",action = action,delay  = 1,onMove = true}