显示标签为“Linux撞鬼集”的博文。显示所有博文
显示标签为“Linux撞鬼集”的博文。显示所有博文

2012年3月30日星期五

Linux撞鬼集4之IFS

前阵子自建CDN,因为有攻击导致某地点某IP不可用。在屏蔽攻击后重新需要给用户重新分配IP地址,但是分配之后一直都没有成功。通过查看配置文件,我们确实配置成新的IP地址了,并且确实已经“成功”的重新启动bind服务。但是用户持续报告不可访问,我们直接找ip地址发现实际上仍然是旧的。

一开始我们怀疑可能是不是TTL错误,或者什么别的原因导致用户访问的DNS服务器没有刷新新的解析记录。经过一阵紧张的nslookup和dig之后,还是没有能成功找出问题所在。最后修改named.conf当中的日志级别为debug,重新启动之后发现实际上有一个区域的解析记录并没有成功加载。日志大概意思如下:

unknown type "xxx.xxx.xxx" for record NS (大概这意思吧)

打开配置仔细一看,原来该区域下面应该有一个NS解析记录,原本应该是:

            NS       xxx.xxx.xxx

结果变成了:

NS xxx.xxx.xxx

这一条解析的意思是,当前要解析的域下面的域名服务器是xxx.xxx.xxx。但是由于前面的空格去掉了,结果按照空格划分,第一个表示二级域名的字段从空变成了NS,而记录类型则从NS变成了"xxx.xxx.xxx",而第三个表示解析结果的字段则从"xxx.xxx.xxx"变成了空。于是这一个域名解析记录加载失败,这也导致了后面更改了的解析记录实际上从未被正确加载。之所以域名服务器仍然返回旧的IP,是因为过期时间比较长,在得不到我们的正确解析时,其它的DNS服务器将沿用原来已经有的域名解析结果。

那么,这个空格为什么消失了呢?如果是某个人手动修改的,其手动意图又是啥?经过调查之后发现,原来有人写了一个修改解析地址的脚本,正是这个脚本造成的问题。而之前就已经发现有此问题,解决的办法是通过运行脚本之后手动添加空格来解决,而这一次却忘了。

具体造成问题的脚本命令是read,这个命令十分神奇,会自动根据IFS来分割字段,然后将字段重新组合成一个字符串。用C#来描述,就是:
return string.Join(IFS[0], Console.ReadLine().Split(IFS, StringSplitOptions.RemoveEmptyEntries));

解决的办法很简单,就是将脚本改写成:

while IFS=""; read LINE
do
#  something
done < afile

关于IFS在while上面赋值,我想了一下,原因应该是这样的:
如果在独立一行修改,则导致从此之后的运行环境发生改变,会影响并改变后续所有命令的默认行为。而如果在while上面赋值,则因为是folk一个子进程,并且在子进程中运行修改的缘故,并不会导致外部(当前脚本)环境的变化,也不会改变后续命令的默认行为。

这种环境变量影响程序行为的问题,在Windows算比较难以注意到的。一般也就是PATH之类的变量会导致能否找到可执行程序,但很少会影响程序本身正常行为的。对于read来说,默认按照IFS分割并重组输入,确实让初学者难以注意到。

2012年3月28日星期三

Linux撞鬼集3之$()内嵌加管道(尚未解决)

由于自己比较菜,写linux脚本的时候总遇到一些很撞鬼的事情,比如说:
$(echo "echo hello")

这个的执行结果是:
hello

但是执行下面这个命令:
$(echo "echo hello | more")

结果你以为仍然一样,结果却是:
hello | more

嗯?为啥是这样的呢?抱歉,我不知道为什么。从我目前的认知来说,我以为$(xxx)的意思是执行xxx命令,并且再次执行该命令的输出。用第一个例子来描述,那就是:
echo "echo hello"

将会输出:
echo hello

而执行 echo hello 的结果,将会是:
hello

也就正好可以解释第一个例子的执行结果。可是到了第二个例子,我们推测
echo "echo hello | more"

的执行结果是:
echo hello | more

而上述语句的执行结果应该是
hello

这是因为该输出只有一行,more不会起任何影响。但是实际上第二个例子的结果却等同于:
echo "hello | more"

嗯,这就奇怪了,bash怎么会如此识别呢?如果说是自动根据IFS来添加双引号/单引号,那也应该是等价于下面这样的命令才对。
echo "hello" | "more"

好吧,我假定$()操作符将会根据输出结果按照IFS分割成两列,第一列作为命令,剩下的都作为参数,这样倒是能解释结果。可是,我要如何写才能得到等价于hello | more的执行结果呢,或者说达到这一个目的呢?

……呃,这个,非常抱歉,google这个玩意儿对于学习linux脚本编程天然鄙视。比如搜索“$()”,结果是木有啊木有,更不要说搜索“$() |”这样的玩意儿了……(泪奔ing)

P.S.:
$(echo "echo hello" | more)这个是可以的,但是这两者其实并不等价。比如说有一个有3行文字的文件a,执行cat a | head -n 1,现实的是一行。但是执行$(echo "cat a" | head -n1),却把所有文字都显示出来了。为啥?不知道,GOOGLE不告诉我……

2012年3月13日星期二

初学配置安装Linux撞鬼集3 - 莫名其妙的squid

知道为什么Linux的维护/开发人员值钱吗?因为Linux世界真的有太多不可思议的事情了:比如说相对于可以调整的参数及其导致的莫名其妙的问题,文档和记录实在是太少了。这里就拿squid来说说吧。

我们使用的Squid服务器之前跑得好好的,突然有一天出现了严重的问题:磁盘空间满了,上去一查,发现在/app/squid/var/logs这个目录里面全是core.12345这样的dump文件,而且几乎是每秒钟都要产生这样的dump。通过gdb打开之后发现,几乎每一个dump都出现了类似如下的情况:

Core was generated by `(squid)'.
Program terminated with signal 6, Aborted.
#0 0x000000373aa30265 in raise () from /lib64/libc.so.6
(gdb) where
#0 0x000000373aa30265 in raise () from /lib64/libc.so.6
#1 0x000000373aa31d10 in abort () from /lib64/libc.so.6
#2 0x00000000004812d1 in fatal_dump ()
#3 0x000000000049c7f9 in xcalloc ()
#4 0x000000000047cc8b in storeKeyDup ()
#5 0x0000000000475b4a in storeHashInsert ()
#6 0x000000000048f280 in storeAufsDirAddDiskRestore ()
#7 0x000000000049092e in storeAufsDirRebuildFromSwapLog ()
#8 0x00000000004375d4 in eventRun ()
#9 0x000000000045da06 in main ()

上面标黄的#3这一行就是问题所在,在其下面的堆栈情况则并不一定。如果你直接搜索xcalloc和squid,几乎是找不到任何线索的。当然,你仔细看网上那些英文帖子的话呢,也会看到有人提示你使用free看看内存是否充足,使用df看看磁盘是否满了。可是你一看,就会发现其实很正常。在Google上搜索这两个关键词,几乎都会指向这样一个Bug,这里面的堆栈和我遇到的问题极度相似。可问题是,人家使用的是3.1系列,而我使用的是2.7系列。而这个BUG的讨论中有人说了,3.1有问题但2.7没有。而且该Bug的描述说的是,如果有人POST了一个很大的文件,超出了内存容量,则可能会出现此错误。但显然,因为这种问题导致频繁当机的可能性太低了,不应该是这个原因。到了这里,似乎又没有任何头绪了。

实际上,如果仔细看第一个帖子,里面还有人贴出了大量的信息,什么Dependencies.txt、Disassembly.txt啊等等。关键的地方其实都不在这些链接里面,而是5楼的回复:
I'm seeing this too; I'm attaching the /var/log/squid-deb-proxy/cache.log, which seems to indicate that squid is attempting to allocate approximately 10PB of memory for swap. This, obviously, fails; hence the abort. 
也就是说,在这个cache.log里面会有线索。Linux先进的地方就在于日志真的很多,dump也很多。但问题也很多,尤其是莫名其妙的问题。那么在这个日志文件里面又说了什么呢?在我这里,这个日志大致如下:
FATAL: xcalloc: Unable to allocate 1 blocks of 4112 bytes!
Squid Cache (Version 2.7.STABLE9): Terminated abnormally.
CPU Usage: 23.655 seconds = 11.627 user + 12.028 sys
Maximum Resident Size: 4665232 KB
Page faults with physical i/o: 0
Memory usage for squid via mallinfo():
        total space in arena:  1020340 KB
        Ordinary blocks:       1020328 KB     38 blks
        Small blocks:               0 KB      0 blks
        Holding blocks:        166348 KB      8 blks
        Free Small blocks:          0 KB
        Free Ordinary blocks:      11 KB
        Total in use:          1186676 KB 100%
显然,我这里的情况和BUG所导致的症状很不一样。前者每次都是分配一个很小的内存——最低只有24字节,最大也不过几KB;而后者则是分配4035364077 blocks of 1 bytes。这就进一步增加了此问题非彼问题的几率了,可还是不知道问题在哪对不?好在这次又有了另一个新的关键词:FATAL: xcalloc: Unable to allocate 1 blocks of bytes

这个问题一搜索,就发现有不少人报告这个问题,除了上面提到的BUG之外,也有像我这种分配一点点内存即报错的(B贴)。当然,也有中文贴,只不过中文贴貌似真没几个人进来讨论,就更不要抱给点营养的希望了。

除了报Bug之外,还有一些文档说这个问题“通常”跟以下两个问题有关:
  1. 这个机器的磁盘交换空间用完了;或者
  2. 这个进程的数据段大小已经达到最大限制了。
对于这两个问题,第一个应该用ps -m或者cat /proc/mem查看。(如果你想知道具体交换文件在哪里,可以用cat /proc/swaps或者swapon -s来查看,坑太多了,这里插播一下。)第二个问题可以通过ulimit -a和ulimit -aH来查看,其中第一个似乎是查看目前的配置,第二个似乎是硬件本身以及kernel编译时的限制。

我执行第一个命令之后,发现空余的交换空间足以将整个进程都交换进去,而正在使用的交换空间则小得可怜(也就100兆),于是第一个问题可以排除。而第二个命令则发现data seg size均设置为unlimited,这个问题也可以排除了。于是这个文档又没有解决问题。

(插播一下,你需要通过运行uname -a来看看你的系统是否64位。如果是32位的系统,也可能会有2G左右的内存大小的限制。)

最后,还是B贴的最后一个说法给出了本问题的终极答案:编译时去掉dlmalloc参数就能解决问题了。而且在这里,你还得到了另一个“有用的”忠告:不要使用你不知道你是否需要的配置参数。这真是一个有用的忠告啊!后面再说这个问题。

真TMD神奇了,这可是从repo下载下来的源代码的默认编译参数啊!那么dlmalloc又是一个什么玩意儿呢?上面那个讨论中说道:
这个参数是为了让那些malloc内存分配函数实现得很糟糕的操作系统所准备的。要知道你是否正好走了这种狗屎运,(在关掉这个参数的情况下,编者注)你会看到Squid不停的增长,但已分配的内存没有变化,而空余内存则一直在增长。
我估计说的是内存地址空间增加,但实际分配的数量很少。相应的,因为内存地址增加导致总虚拟内存增加,而空余内存则因为总虚拟内存-实际分配内存的计算,导致空余内存不停增加的假象。

如果你想知道具体什么是dlmalloc,这篇文章会告诉你这是一个叫做Doug Lea的同志,在1987开始写的内存分配函数。由于该邮件是1996年从德语翻译过来的,所以估计在那时候已经基本稳定了。换句话说,这是一个古董。而这个古董级的文件可能根本就无法分配超过2G的内存,甚至在前面那个B贴中说无法超过1G,而我的实际情况也是似乎无法超过1.2G。可是这个问题在很多地方都没有提及,尤其是中文的编译参数详解说明。

看到了吧,Linux里面可以动的东西很多,坑也就很多。大家一定要注意了,LINUX下面的默认编译安装方式,有可能会要了你的亲命。为了避免出现像我这里描述的问题,去掉默认参数也许都不是稳妥的。因为有可能某些参数一去掉,这种隐藏的Bug是没有了,但性能就急剧下降,甚至你无法证明你的系统必须要有某些参数才能够工作。于是,保险的办法应该是了解都有哪些编译参数,每一个参数是干什么的,你的系统有什么限制,你需要什么样的优化。这才是刚才那句忠告的终极含义!

换句话说,你需要搞清楚什么事内存分配,malloc和dlmalloc的关系,还要英语过硬知道怎么搜索。这,就是一个linux系统应用维护人员所需要具备的基本素质,可不是Windows人员拷贝安装就可以搞定的。这还不包含安装完毕之后的各种配置呢!




2012年3月6日星期二

初学配置安装Linux撞鬼集2


什么?0 == true?
习惯了各种C系编程,很自然的认为0表示false,1(或者其它非零非空值)表示true。于是当你看到一个脚本如下所示:

#!/bin/sh

function ret0 { return 0; }

if ret0; then
    echo "HELLO! The condition is TRUE!"
else
    echo "HELLO! The condition is FALSE!"
fi

这时候,如果你继续使用C系列语言(乃至绝大多数常见语言比如js)的思维来思考的话,你会认为输出应该是

HELLO! The condition is FALSE!

可是当你运行这段脚本之后,你会发现结果居然是TRUE……当然了,自圆其说还是可以的:C/ASM的标准中,返回0表示没错,其它值表示有问题。既然经常要判断是否正常,用0表示true也是可以理解的。这个让我想起了在飞机上和印度人聊天,当他点头的时候我都不知道到底算是Yes呢还是No,我看他自己也搞不清楚,经常是先点头后摇头,或者先摇头后点头。

总结:
Linux/Unix的Bash尽管是用C写的,那也不代表其脚本语言就遵循了这些语言的逻辑标准。

P.S.:
Q:为什么成功通常是0呢?
A:因为出错的原因多种多样啊,比如1表示文件不存在,2表示目录不存在,3表示你太罗嗦了……

Q:可是,这还是不能解释为什么选择了0这个值,而不是其它值,比如1来表示成功啊。
A:因为你必须写某个判断语句来判断之前的操作/调用是否成功,比如说:
if (ret == 0) printf ("success");
0这个数字比其它的数字都好记,但这还不是最重要的。最重要的是,判断是否为零,在汇编当中比判断任何其它数字都要简单和快速(相对于机器而言),至少在x86系列肯定是这样的。比如:
; if ax == 0 goto label
xor ax, ax  ; 当然了,这里的ax就毁了。但通常后边不会再用,要用就先备份好了。
jz label

; if ax == 1 goto label
cmp ax, 1
jz label

前者最短只有2个字节,后者最短要5个字节。早期的CPU中(比如386),时间周期也会有非常巨大的差别。前者第一句话可能1个时钟周期就完事了,后者可能得要4个时钟周期。啥?看不懂?复习汇编去。

(待续)

2012年3月5日星期一

初学配置安装Linux撞鬼集

对于刚踏入Linux的同学,肯定会遇到不少“撞鬼”的事情。所谓撞鬼,就是说:“邪门了,怎么就是不行呢?”为了让大家不要重蹈覆辙,这里记录一些我遇到过的事情。

为什么不能往特定目录记录日志?

这个问题的来由是这样的:某天我需要在内网一台已经配置好的Linux服务器上面,配置一整套与线上正式环境完全相同的测试环境。其中有一个步骤就是安装HAProxy,安装的过程基本还算顺利。配置并运行后发现,服务倒是起来了,但日志却没有记录到指定的文件当中。

需要说明的是,HAProxy这里选用了syslogd的方式,在/etc/sysconfig/syslog里面开了-r -x参数,然后在 /etc/syslog.conf 里面添加了一行“local3.* /mydir/logs/haproxy.conf”,同时修改好haproxy的配置"log 127.0.0.1 local3 info"。本以为这么配置一下就大吉了,但是实际上却发现该目录下面没有该日志文件,这就诡异了。由于一开始配置错误,选择了local7,于是日志记录到了/var/log/boot.log里面去了。所以很明显,该问题不是因为HAProxy或者syslogd配置错误导致。通过使用ls -l以及getfacl来查看各个目录和文件的情况,发现测试环境和正式环境是完全一致的。那么,到底撞什么鬼了?

经过一番查找,发现了这么一篇FAQ,发现原来:

  • /etc/syslog.conf的配置里面不应该使用空格而是"tab",来区分前面的local3.*和后面的目录;
  • syslogd不会自己创建文件,要你自己先创建文件(比如用touch /mydir/logs/haproxy.log)。

我第一个念头就是“真狗屎”,你说这叫什么事啊,tab才行空格就不行,还要自行先创建文件。好吧,为了安全,还是自己动手丰衣足食好一点。由于凑巧使用的是tab而不是空格,第一个问题我没有遇到。而第二个问题,我确实没有创建该文件,只好老老实实touch了一下。可是touch完了,这个文件中还是没有任何的日志,哪怕我狂刷浏览器。这就奇了怪了!

经过咨询周围了解linux的同学,还有当初架设线上服务器的前同学,最后终于发现……他们也不知道为什么。非常沮丧的我只好“胡乱瞎试”:

  • 既然我自己创建的目录里面不能记录日志,但是/var/log可以记录,那肯定是这两个目录的某种差异,甚至最可能的就是权限差异造成的;
  • 既然/var/log可以记录,那肯定可以在这个目录当中创建新的log;
  • 既然可以配置,那干脆把所有日志都记到某个文件当中好了。

于是配置/etc/syslog.conf,添加一行“*.* /var/log/debug2.log”,并touch之,然后service syslog restart。这时候通过more /var/log/debug2.log就会发现,里面有那么一行:
某年某月的某一天 时分秒 机器名字 setroubleshoot: SELinux 正在阻止访问带有默认标签 default_t 的文件。 For complete SELinux messages. run sealert -l 某个GUID

好吧,我手贱,语言选了中文,懒得改回去了。英文版本的应该是“…… SELinux is preventing access to files with the default label, default_t. ……”。

上述信息的含义,是安全上下文环境设置不正确导致的,通过使用命令 ls -Z /mydir/logs可以看到该目录下面的上下文设置信息,如下所示:
-rw-r--r--  root root root_u:object_r:default_t      haproxy.log

根据某处搜索的结果,发现setfattr -x security.selinux可以解决这个问题,很是高兴。但结果一运行,就会告知“没有足够的权限”。NND,我可是用的root啊!经询问才发现,原来是开启了SELinux的缘故。于是有人提供了另一个方案,就是使用setup将selinux给禁用了。

禁用这个方法肯定可以,但显然不是最好的办法。所谓SELinux,就是为了提高系统安全性的东西。禁止它,一定是比较糟糕的实施方式。那应该怎么办呢?

嗯,很简单,参照/var/log这个目录,及其内部文件的响应设置即可(使用命令ls -Z查看)。具体一点,就是执行:
chcon -u system_u -t var_log_t 文件或目录路径
或者
chcon -u system_u -t var_log_t -R 目录路径

其中后者会将该目录下所有的子目录和文件都设置成相同的安全参数,经过这样的设置,日志的记录就成功了。

总结:

  • 配置啥的都对了,也有可能因为各种安全性的原因而无法正常运行;
  • 由于Linux的设计思路是“没有错误”就不输出,但实际上有些时候因为配置的问题,一些错误信息也是不输出的,导致你总觉得正常,但偏偏就不正常。此时一定要想办法让其尽可能输出详细的信息,例如将所有系统/应用的日志都输出到某个日志文件当中。

(待续)