手 Q 红包工程师过去一年踩过的坑

时光似箭,岁月如梭,恍然间,已入职一周年了。这一年应该学习了不少东西,但决定要好好总结一下自己的时候,却不知从而谈起。

这一年来,编写过 CGI,开发后台 middle,写过批处理程序,编写过 shell 脚本,线上版本迁移,ios Demo 开发,参与过手 Q 红包项目,这一年,很忙,但也很充实。甚至自嘲长期处于一种“忙成狗,累成牛”的状态。

一年了,似乎也没学到多少东西,还有很多知识等着自己去汲取,去挖掘,继续努力,不断前行。工作中遇到的一切,几乎都是从 0 开始,故难免会走很多弯路,也曾踩过无数的坑。接下来,我与大家谈谈平日里在项目中遇到的问题以及待优化之处。其中,有些很诡异的问题未定位,欢迎各位解答。若有异议,欢迎拍砖哈!!!

接下来,比较常见的问题,放在 Comm 部分,接下来 Middle 开发中遇到的问题,随后分别介绍 CGI、批处理、Shell、Mysql 和 IOS 中遇到的问题。

1 Comm

1.1 XML 解析

问题描述:相信很多童鞋都遇到过类似的问题,即当 Xml 文件中存在多个同样节点时,则它们之间不能有任何注释,否则读取 xml 失败。例如:在较为常见的代码中,取下一个节点使用 NextSibling【NODE_NEXT】方法。如下若是,读取 xml 文件失败,导致 core,且对应的 server 会被 middled 进程无限次拉起。

TiXmlNode* NODE_NEXT(TiXmlNode* pNode)

{

    return pNode->NextSibling();

}

string NODE_ATTR(TiXmlNode* pNode, const char* attribute)

{

    return pNode->ToElement()->Attribute(attribute) ? pNode->ToElement()->Attribute(attribute) : "";

}

pTransNode = pTransNode->FirstChild("transaction")->FirstChild("variable");

while (pTransNode)

{

         string nodeName = NODE_ATTR(pTransNode, "name");//若注释,则会报错

         pTransNode = NODE_NEXT(pTransNode);

}

<transaction>

         <variable name="spid_list" value="2000000501"></variable>

         <!--- 该处存在问题,会发生 core -->

         <variable name="watch_word" value="abcdefg"></variable>

</transaction>

问题原因:循环读取相邻节点时,则会读取下一行注释,而 Node->ToElement()会 core。

解决方法:使用 NextSiblingElement 方法获取相邻节点,编译所有涉及读取 XML 文件的项目并做出修改。

1.2 高危函数

问题描述:相信很多人拉取代码分支时,都收到过自动化编译的高危函数告警。其中存在 strcpy、sprintf、strcat、memory leak、野指针等

问题原因:历史遗留

解决方法:

1、改用 snprintf 安全函数,其能够保证标缓冲区末尾为’\0’,故使用 snprintf(szBuf, sizeof(szBuf), srcStr);若使用 snprintf(szBuf, sizeof(szBuf)-1, srcStr);,则会减少一个空间区域。

2、改用 strncpy,其不会在缓冲区最后添加结束符。为防止越界操作,故常使用时有两种用法:

1) char szBuf[32] = {0};// 全部初始化

strncpy(szBuf, src, sizeof(szBuf)-1);// 仅考虑前 sizeof(szTime)-1,故最后一个字节为 \0

2)

  char szBuf[32];

  strncpy(szBuf, src, sizeof(szBuf)-1);

  szBuf[32] = ‘\0’;

1.3 签名

问题描述:针对 url 请求串,service 一般都将其读取至 map 中,方便后续处理。若进行 md5 签名时,一般使用类似 string map2str(map& reqMap) 得到 ASCII 升序的字符序列,继续计算 md5 值,签名也比对成功。当一段时间后,签名验证却未能通过。

问题原因:map 为(param1,abc==),(param2,100),若采用通用的 map2str 方法,其实得到字符串为 param1=abc%3D%3D&param2=100。或许还存在标准 URL 编解码的问题,在此不再赘述。也许你会问,什么情况下会遇到=&等特殊字符。

1、url 嵌套,如 (reqText, uin=boy&age=20&sex=1)

2、字符串进行 Base64 编码。Base64 要求把 3 个 8 位字节(3×8=24)转化为 4 个 6 位的字节(4×6=24),之后在 6 位的前面补两个 0,形成 8 位一个字节的形式。 如果剩下的字符不足 3 个字节,则用 0 填充,输出字符使用'=',因此编码后输出的文本末尾可能会出现 1 或 2 个'='。

解决方法:1、尽可能打印关键 debug 日志 2、重新封装 genSign(map& reqMap) 函数,对源串不编码、不转义。 3、牢记签名一般将 key 按 ASCII 升序 [map 即可实现],末尾追加“key=”。然后进行 md5。

1.4 服务迁移问题

问题描述:因机器更换等原因,需要对线上运行的服务进行迁移。当提供了迁移说明文档,运维按照说明文档执行时,却出现了很多未预料到的情况。

问题原因:

1、数据库无访问策略

2、多台机器上的服务配置不完全一致

3、灰度方案考虑不全面

4、mysql 版本不一致 [ 很奇葩]

5、业务异常考虑不全面

解决方法:

1、与运维多次确认网络访问策略并进行实地验证

2、反复对比配置,确认符合预期

3、在开发环境进行充分的验证

4、加入告警,跟踪实时日志,以便异常出现时能够及时发现

1.5 rsync 配置

问题描述:1、在 rsync 服务端所在机器,ps 未查询到 rsync 进程。

2、密码文件未起作用

问题原因:1、rsync 同步文件有两种方式 2、密码文件属性必须为 600

解决方法:

1、拷贝命令

rsync -av --port=873 --password-file=./passwd_rsync ${filename} egbertzhang@10.12.196.190::backup

其中,backup 为/etc/rsync.conf 中的模块名,路径为 path=/data/egbertzhang/

密码文件属性必须为:

-rw-----  1 egbertzhang users    7 2015-06-04 10:00 passwd_rsync

2、通过 ssh 登陆的方式

rsync -ave "ssh -p 36000 " ${filename} egbertzhang@10.12.196.190:/data/egbertzhang/

然后输入登陆密码就传送 ok。

若想通过脚本传文件或者不想每次都手工输入登陆密码,则可使用 expect 方法,如下:

./expass.exp "rsync -ave \"ssh -p 36000 \" ${filename} egbertzhang@10.12.196.190:/data/egbertzhang/tmp" "${passwd}"

其中 expass.exp 实现不在此赘述,见附件。

1.6 时间转换

问题描述:设计到月表时,一般情况下,都会根据当前时间戳获取上一个月的月份,简单实现如下:

curTime = time(NULL);

localtime_r(&curTime, &tm_now);

tm_now.tm_mon-=1;

mktime(&lastTime);

若当前时间为 2014-12-31 23:59:59,则转换后获取的时间为 2014-12-01 23:59:59,即当时 12 月,并不是预期中的 11 月份。

问题原因:11 月有 30 天,而 12 月份有 31 天

解决方法:将当前时间 day 置为 1 日,然后进行相减运算。

1.7 跨月查询

问题描述:查询当前月表后,获取当前时间,设为 2015-4-30 23:59:59,故需要查询 4 月份的数据库表,若无对应的数据记录,且 DB 查询时间超过 1s[ 此情况很常见]此时,时间已经大于 2015-5-1 00:00:00。因此查询前一个月库表时,实际上会继续查询 4 月份的数据。因此,3 月份的数据永远也不会被覆盖到。若该业务逻辑依靠月表保证重入逻辑,则可能存在严重问题。

// 查当月

string month = getMonthFromNow(0);

int flag = queryPayRelation(month);

if (flag == 0)

{

         // 往前查一个月

         month = getMonthFromNow(-1);

         flag = queryPayRelation(month);



         if (flag == 0)

         {

                  // 往后查一个月

                  month = getMonthFromNow(1);

                  flag = queryPayRelation(month);

         }

}

问题原因:历史问题

解决方法:以一个固定时间为基准,左右偏移各一个月。

1.8 诡异问题

问题描述:1、脚本提示错误,但是仔细检查后未发现异常 2、xml 标签读取失败 3、输出日志顺序混乱

问题原因:1、全角符号问题,不易发现 2、XML 标签不对应 3、涉及标准输入输出时,将 stderr 重定向到标准输出,如 “> /tmp/test.log 2>&1”

解决方法:细心、细心、再细心

2 Middle

2.1 新建 server

问题描述:1、新建 server 时,发现 service.so 无法被加载 2、打印 log,但是文件大小为 0

问题原因:

1、新建 server 时,注意事项

l 在 trpc.conf 中新增配置项

l load all

l 修改日志级别,setlog server debug

l xxx_server,必须生成 xxx_service.so.

l 需要手动 start server

解决方法:

1、见注意事项

2、确定是否有写权限?server 是否运行打印逻辑?最后一种情况下可以考虑了,那就是磁盘空间满了。这么小的概率却被我撞了 n 次,包括 DB 插入数据报错,其他都是 log 中无内容。

2.2 SQL

问题描述:参与过的一些项目中,sql 查询经常会 core,常见的有以下三种:

1、

strncpy(stOrder.recv_name, row[6] : "", sizeof(stOrder.recv_name)); 

stOrder.status = row[7] : 0

2、

strncpy(stOrder.recv_name, row[6] ? row[6] : "", sizeof(stOrder.recv_name)); 

stOrder.status = atoi(row[6]) : 0

3、

strncpy(stOrder.send_name, row[5]? row[5] : "", sizeof(stOrder.recv_name)); 

strncpy(stOrder.recv_name, row[6] ? row[6] : "", sizeof(stOrder.send_name)); 

stOrder.status = row[7] ? atoi(row[7]) : 0

问题原因:

1、有些数据库创建时,varchar 字段 default NULL,int 字段 default NULL.导致取出的字段值 row[x] 为 NULL,故会 core

2、row 下标写串了,随着字段的增多,且不断修改,很容易出现该问题,由于 row[6] 可能为 NULL,而 atoi(row[6]) 会 core

3、字段名写串了,当数据字段长度不一致时,会发生阶段,导致数据错误。

解决方法:

1、建议数据库表创建时,varchar default “”, int default 0,或者使用 NOT NULL,尽可能不使用 deafult NULL.

2、server 中拼写 sql 语句时,建议都使用 row[x] ? atoi(row[x]) : 0 / row[x] ? row[x] : “” 判空

3、建议使用类对象,而不是 C 的 struct 存储。如下所示:

SpOrder.m_spid = row[iCurIndex] ? row[iCurIndex++] : 0;

SpOrder.m_sp_uid = row[iCurIndex] ? atoi(row[iCurIndex++]) : 0;

这样解决了上述的 core 问题。

2.3 xml 解析库

问题描述:在做 tlinux64 版本的迁移过程中,发现各个模块使用的 xml 库不统一。有使用 libminixml.a,也有使用 libtinyxml.a,为方便起见,故将两个库全部参与链接。当时解析 xml 时无故 core。

问题原因:至今未查明原因,可能不兼容

解决方法:后续都统一使用 libtinyxml.a,对应的头文件为 tinyxml.h

2.4 core 调试

问题描述:对于 linux 基础不扎实的我来讲,且刚入职不久,遇到 server 重启问题,而且没有 core dump 文件。

问题原因:1、或许担心 core 文件太大,为防止 server 进程重复被拉起,为避免硬盘空间被大量占用,大多数情况下,core dump 开关处于关闭状态。若不分析代码的话,不容易发现问题。

2、日志句柄未分配空间

DECLARE_SO_INIT()

{

    try

    {

                  // 读取配置项

        gPtrConfig = new GlobalConfig("*.xml");



        // 初始化日志指针

        gPtrAppLog = new CftLog();

        ..........

         }

         catch(CException& e)

    {

                  gPtrAppLog->debug("server start ...");

        throw;

    }

}

解决方法:

1、查看 shell 占用的资源 ulimit -a

2、打开 core dump 开关 ulimit -c unlimited

很多情况下,core dump 开关未打开的

ulimit -c 1024 打开 coredump

3、进程启动前,尽量使用 trpc_debug_log 打印,catch 用日志句柄打印时,至少应该判空

2.5 封装问题

问题描述:曾经遇到很郁闷的问题,在大量的代码中,很难发现问题,如下:

Parameters paraTmp;

paraTmp["host"] = “127.0.0.1”;

......

结果发现 paraTmp[“host”]为空,然后打印 log,printf(“host=%s”, paraTmp["host"]);发现依旧为空。

问题原因:其实 Parameters 类是这样重载 []操作符的

string operator[]( const string& key )const

{

         return m_map.find(key) != m_map.end() ? m_map[key.c_str()] : “”;

}

因此,paraTmp["host"] = “127.0.0.1”;仅仅修改的临时 string

解决方法:将返回类型用 const 修饰。

2.6 加密/无密 接口

问题描述:有时候,调用其他 service 接口,莫名其妙的报错,尽然说缺少参数。

问题原因:url 请求串未加密,调用加密接口。加密 url 请求串后调用了无密接口。

解决方法:1、先确认参数名是否拼错? 2、确认有密接口还是无密接口 3、看被调用服务日志

2.7 缩容扩容

问题描述:春节前,为备战微信红包,核心系统进行了扩容,而目前面临缩容问题。而我们的对账平台可以支持逻辑 DB 或者实 ip 扫描数据库,从而继续业务对账。但是有时候在逻辑 DB 时,不确定应该选用哪个。如下图所示:

同样是读取 c2c_db_05.t_user_order_5 表,但究竟选用 NewUserOrder/OrderDBOne/OrderDBTwo,还是选择 UserOrderDBOne 呢?

问题原因:DBA 的数据库管理系统供多个业务使用,故场景不同,库表也不一致。

解决方法:请教 DBA

NerUserOrder:仅仅保留近 3 个月的数据

OrderDBOne:主要是订单数据,即 t_order。而其中的 t_user_order 可能之前的账务组需要,不必考虑

UserOrderDBOne:订单拆分,数据来源于 t_order。可能会做迁移,对业务透明。

3 CGI

3.1 CGI 输出

问题描述:CGI 的逻辑时,调用一个脚本程序将文件 rsync 到目标机器,成功后通知页面执行成功。而根据新增需求实现,部分情况下,调用新脚本程序 rsync 到目标机器,其他继续走老逻辑。最后发现,走新逻辑,CGI 正常返回,但是页面无法正常显示。而老的逻辑则可以。由于 apache 重定向了标准输入输出,故主要定位方向是查找新逻辑输出的多余数据。

问题原因:CGI 调用新的 shell 脚本将文件 rsyn 到目标机器,但未重定向返回结果日志,从而导致返回给页面时多出很多 rsync 的结果数据,不符合 http 请求响应报文格式,故报错。

解决方法:对比新旧脚本,新增 rsync ... > /tmp/rsync.log 2>&1

4 批处理

4.1 crontab 配置

问题描述:常住内存的批处理程序,中间中断一段时间未发现。

问题原因:因异常逻辑触发隐藏 bug,导致 core,随后没有机制可以保证再次启动。

解决方法:1、即使常驻内存的批处理程序也需要定期检测是否运行中,若为运行,则启动

2、批处理程序增加文件锁机制,crontab 配置定期启动批处理程序

PS:脚本中需要写为绝对路径。若需要相对路径,则需设置环境变量。

5 Shell

5.1 导出 DB 记录

问题描述:select 多列时,若将结果集重定向到一个文件中,结果集并不是多行。

db_result="select Fconfig_id, Ftask_name from db.t_table limit 2;";

echo $db_result >> $file_name / echo ${db_result} >> $file_name

//2001 2 500 10101 2 500 10102 2 500

问题原因:被单引号''括住的内容,将被视为单一字串。在引号内的代表变数的$符号,没有作用,也就是说,他被视为一般符号处理,防止任何变量替换。

name=tom; echo '$tom' # $tom

被双引号""括住的内容,将被视为单一字串。它防止通配符扩展,但允许变量扩展。这点与单引数的处理方式不同。

name=tom echo "$tom" # tom

解决方法:为防止 \t,\n 等特殊字符会被转义,需要加双引号,即 echo "$db_result" >> $file_name

//2001 2        500

//10101        2        500

//10102        2        500

5.2 变量作用域

问题描述:第一次写 shell 脚本时,使用了很多 function 方法,也定义了很多变量。当其中一些变量被无故的覆盖了。

问题原因:shell 中变量默认为全局变量,且全局作用。

解决方法:若需要局部变量,则需要使用关键字 local 声明。

5.3 MD5 计算

问题描述:若计算一个字符串"md5test",则使用命令 echo md5test | md5sum | awk '{print $1}',得到 md5 值为 689a850ebe8e2cf48429c9f9879e4300。但是在代码中得到的 md5 值是 82da61aa724b5d149a9c5dc8682c2a45。莫非代码有 bug?

问题原因:使用 echo 时,不加-n 选项,字符串默认会包含\n。

解决方法:增加-n 选项,如 echo -n 'md5test' | md5sum | awk '{print $1}';

5.3 Other

1、Shell 函数返回值必须是字符串。

2、字符串赋值时,=号左右不能加空格

expr $g_line_no + 1;#+ 号左右必须加空格,否则当做字符串处理。shell 内所有符号【赋值=除外】都需要有空格。如 lt gt 等

3、val1="test01";# 带双引号

val2='test02';# 不带

shell 中,如果用单引号,即变量不带有“”,即只是字符串的内容。

4、在 shell 脚本中,若有 while、done 语句,在 done 关键字后需要加换行符。如果需要换行,不能在行末尾加空格,否则空格将会被转义。

5、sz filename

如果 filename 最近未修改过。那么发送到本地时,修改时间不变。故不能仅仅按照修改时间排序。

6、赋值操作符

1) "="

在 makefile 中,make 会将整个 makefile 展开后,再决定变量的值。即变量的值将会是整个 makefile 中最后被指定的值,如下所示:

x = value

y = $(x) bar

x = xyz

在上例中,y 的值将会是 xyz bar ,而不是 value bar

2) ":="

变量的值决定于它处于 makefile 中的位置,而不是整个 makefile 展开后的最终值。

x := value

y := $(x) bar

x := xyz

在上例中,y 的值将会是 value bar ,而不是 xyz bar

6 Mysql

6.1 库表创建

问题描述:如 2.2 所述,数据库创建时字段默认 NULL 的话,则使用 mysql API 得到的 row[x]为 NULL。

问题原因:default NULL,任何类型都会插入空指针

NOT NULL,则 varchar 类型会插入空字符串,而 int 类型会插入 0

解决方法:为安全起见,建议 varchar default “”,int default 0,或者使用 NOT NULL.

6.2 count/sum

问题描述:如下所示,当记录不存在时,sum(x) 返回的不是 0,而是 NULL,与自己预期的不一致。

mysql> select Fsql_limit from cft_db_check.t_task_conf;

+------------+

| Fsql_limit |

+------------+

|     500 |

|     500 |

|     500 |

+------------+

mysql> select count(Fsql_limit),sum(Fsql_limit) from cft_db_check.t_task_conf;

+---------------------+--------------------+

| count(Fsql_limit) | sum(Fsql_limit) |

+---------------------+--------------------+

|              3 |         1500 |

+----------------------+-------------------+



mysql> select count(*),sum(Fsql_limit) from cft_db_check.t_task_conf where Fsql_limit = 1000;

+----------+-----------------+

| count(*) | sum(Fsql_limit) |

+----------+-----------------+

|        0 |            NULL |

+----------+-----------------+

问题原因:count 永远会返回整型,sum 在无符合条件记录的情况下会返回 NULL,而不是 0.

解决方法:使用 SELECT COALESCE(SUM(Fsql_limit),0) ...

6.3 update

问题描述:看一下下面的 update 语句,

update c2c_db_inc.t_spm_batopt set Fstatus=1,Fspid='1215224601'and Fmodify_time=now() where Fbatch_id='201506010187728312';

这个是简单的更新语句,至此,你看出什么问题了么?我找了 n 个人看,但都没有看出有什么问题。其实,更新成功了,但是结果却是未预料到的。

mysql> update c2c_db_inc.t_spm_batopt set Fstatus=1,Fspid='1215224601'and Fmodify_time=now() where Fbatch_id='201506010187728312';

Query OK, 1 row affected (0.00 sec)

Rows matched: 1  Changed: 1  Warnings: 0

执行成功后,Fstaus 设置为 1,ok,没问题。但是,你会发现,Fspid 被设置成'0',而 Fmodify_time 根本没有得到更新。

问题原因:多个设置域应该用逗号,分开,至于为何不报错,且 Fspid 设置为'0',原因未知。莫非是 Mysql 的 bug?

解决方法:

update c2c_db_inc.t_spm_batopt set Fstatus=1,Fspid='1215224601', Fmodify_time=now() where Fbatch_id='201506010187728312';

7 iOS

1 暂时未整理,待续

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

张学林的专栏

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户2442861的专栏

2014网易实习生招聘面试题

http://blog.csdn.net/silangquan/article/details/18969875

751
来自专栏码神联盟

碎片化 | 第四阶段-40-Struts组件分类讲解-视频

如清晰度低,可转PC网页观看高清版本: http://v.qq.com/x/page/o0567s4azx0.html ---- ---- 版权声明:本视频...

3229
来自专栏Android群英传

Android Native Crash 收集

本文是『张涛的NDK之旅』,本来很早以前就有很多读者希望我能写一些关于MDK的文章,但是由于我本身对NDK不熟悉,所以找来了同事张涛的文章。欢迎大家关注他的博客...

521
来自专栏对角另一面

读Zepto源码之Ajax模块

Ajax 模块也是经常会用到的模块,Ajax 模块中包含了 jsonp 的现实,和 XMLHttpRequest 的封装。 读 Zepto 源码系列文章已经放到...

1980
来自专栏Pulsar-V

C++ CGI编程(一)Ubuntu Apache环境配置

首先,修改apache2.conf <Directory "/var/www/cgi-bin"> AllowOverride None...

3475
来自专栏小樱的经验随笔

凯撒密码加解密及破解实现原理

概念及原理 根据百度百科上的解释,凯撒密码是一种古老的加密算法。 密码的使用最早可以追溯到古罗马时期,《高卢战记》有描述恺撒曾经使用密码来传递信息,即所谓的“恺...

2806
来自专栏较真的前端

关于客户端存储的前端面试题总结

2037
来自专栏吴柯的运维笔记

Linux下常用的shell脚本整理

<转>分享下看到比较好的关于常用的shell脚本,供大家学习: 1、脚本之间互相调用与传递参数   "1.sh"的脚本,接受参数。如下,如果有一个...

3654
来自专栏DOTNET

【翻译】MongoDB指南/CRUD操作(二)

【原文地址】https://docs.mongodb.com/manual/ MongoDB CRUD操作(二) 主要内容: 更新文档,删除文档,批量写操作,S...

2888
来自专栏个人分享

MongoDB 3.0.6的主,从,仲裁节点搭建

在MongoDB所在路径创建log和data目录 mkdir log mkdir data

811

扫码关注云+社区