软件开发: 07 2010存档

先上代码,很少,就两个类。

  #include <iostream>

  using namespace std;

  struct Pig
  {
      virtual void call(void)
      {
          cout << "Pig\n";
      }

  };

  struct SmallPig : public Pig
  {
      virtual void call(void)
      {
          cout << "Small Pig\n";
      }
  };

  int main(int argc, char* argv[])
  {
      Pig* p = new SmallPig;
      p->call();
      delete p;
  }

打印出什么?这个简单,了解c++的都能答对:打出 "Small Pig“,因为是虚函数嘛。

好,现在改改Pig类:

  struct Pig
  {
      Pig()
      {
          call();
      }

      ~Pig()
      {
          call();
      }

      virtual void call(void)
      {
          cout << "Pig\n";
      }

  };

再运行,就不是打出3行“Small Pig”了,而是:

Pig

Small Pig

Pig

为什么?因为构造函数和析构函数特殊,在它们里面this指针只能当成自己用(而不是当成子类),所以调用虚函数的结果是调用了父类的实现。

这个问题造成了今天的bug,花了不少时间。其实这个注意事项在《Effective c++》里是有的,我也看过,但是....开发中谁还记得那么多条条框框?还是实际犯错印象比较深刻。

有人问了:如果我把call改成纯虚函数会怎样呢?更郁闷,g++编译的时候就会提示构造函数里的call“找不到实现”!

为了发挥多核机器的威力,可以用多进程或多线程的办法,由于多进程往往涉及共享内存等IPC问题,所以很多人都倾向于选择多线程,并以此为灵丹妙药。但多线程并非万能,它虽然使用方便,却也有硬伤——线程一死,会牵连其它。孙子曰“不尽知用兵之害者,则不能尽知用兵之利也”,不了解多线程的缺点,也就不能很好的使用它。

我们项目中有一个daemon,功能是转发并处理消息,为了能看到daemon运行的细节,我们还做了一个monitor线程,由该线程通过某个端口提供简单的web服务,这样就可以直接用浏览器查看daemon的运行状态(比如处理了多少消息,丢弃了多少等)。后来,monitor线程出现了一个bug,造成线程挂掉——于是造成了整个daemon挂掉。这下郁闷了,daemon本身是很重要的,而monitor是不那么重要的,现在是次要部分的bug拖累了重要部分的运行

这就是多线程程序的缺陷。如果多个线程做的是同样的事情,那还尚可;但如果多个线程,有的做这件事,有的做那件事,而且事情的重要程度不同,那不重要的线程由于代码错误或其他原因死了,其它的线程——包括执行重要功能的——也只能跟着挂。这在健壮性上肯定是不好的。apache采用多进程应该也是出于这样的考虑,因为它的module可能是用户自己写的,可能并不稳定,但由于module不稳定而挂掉整个apache,显然不应该。当然,apache2开始支持多线程,但即使这样,它默认还是多进程的,并没有整个倒向多线程。

也许有人会说:你代码写好一点,不要有bug,多线程不就没事了吗?首先,我们讨论的是软件健壮性的问题——怎样在坏了一部分以后其它部分还能工作,而不是软件正确性的问题——怎样写正确的代码。不是一个方向的问题,并不矛盾。其次,软件不可能没有bug,我们如果能把不同杀伤性的bug通过不同进程把它们隔开,就能降低影响,这跟挖掘bug的目标是一致的——都是为了增加软件的可用性。

所以,多线程并非万金油。为了健壮性,可以考虑把不同性质的任务分到不同的进程上,再由父进程统一管理。而在这些进程之下,可以再有多线程。当然,这样开发就复杂了。

为了在32位机器和64位机器之间传递状态消息,我们给消息格式做了padding:

struct StateMsg
{
uint32_t msgType;
uint32_t padding;
uint64_t msgID;
};

这样,不管是在32位机器上还是64位机器上,消息的大小都是16个字节。开始一切正常,直到后来我们发现有问题:程序里会比较本条状态消息与上一条有什么不同,如果不一样,要清空路由表;如果一样,就说明状态没有变化,于是不做任何操作。而错误出现在我们的消息比较用的是memcmp:

memcmp(oldMsg, newMsg, sizeof(struct StateMsg));

这下连padding也加入比较了,但是padding我们却没有对它赋初值!结果,每条消息都和上一条不同,路由表于是被频繁的清空....
padding本身是用来对齐的,对业务没有任何意义,所以赋值的时候容易忘掉它。教训啊。



====== 2010.12.1 ======

fix这个bug以后,在生产上我们上线了一部分机器,但是发现流量不均匀的问题依旧。照理说路由表被清空的已经不怎么频繁了,怎么还会流量不匀呢?

和架构师一起查这个问题又查了两周,才发现:原来我们的路由表是分段的,每个段都有单独的RoundRobin计数器,一旦路由表清空,这所有段的RoundRobin计数器都置0。线上的服务器很多,我们估算有50台,对应路由表中有400个段!一旦清空,假如来了400个消息,正好均匀分布到400个段上(这有可能发生),于是,这400个消息都从头开始RoundRboin。所以,一次清空路由表的影响,被这个分段机制给恶化了。

分段还是要的,解决方案不变,还是把所有的服务器上的软件都升到fix bug的最新版本。只要“路由表频繁清空”这个根源解掉,后继的问题就都会消失。

找了个周一的下午,和PE一起升级,最后看到流量慢慢恢复均匀了。长达半年的bug终于落地。