页面

分类

VC C1047 链接错误一例

2017年8月28日星期一, by wingfire ; 分类: 计算机技术; 0 comments

BRE team在编译一个已发布版本的源代码时遇到链接错误:

fatal error C1047: The object or library file 'XXX.obj' was created with an older compiler than other objects; rebuild old objects and libraries

编译器是vs 2015 RTM, 按照网上的提示,清理编译环境,重新build. 其实,仅仅就错误信息提示,就足以作出猜测,清理环境,问题就可能得到解决.可实际上问题依旧.开启link的/verbose选项,因为有大量的输出,并未得到有效信息.反复google,没有进展。可是在反复尝试过程中发现,并非所有项目都有此错误,进而发现某项目中并非所有obj都有此错误,最终在某项目中同时存在无错和有错的obj.因为其错误信息报告XXX.obj是老编译器生成的,于是尝试比较有错和无错的两个obj,发现头部并无显著不同,因为是ANONYMOUS OBJECT,所以dumpbin也看不到更多信息.link -disasm 也无法反汇编。无奈通过命令行,让link分别link有问题和没有问题的obj,并重定向输出到两个日志文件,然后人肉比较两个日志文件.

比较过程中发现,错误信息的输出是在第一次处理到sqlite3这个静态库时报告的.sqlite3是一个外部静态库,但并不是唯一的一个.根据日志前面已经成功处理的静态库输出来看,应该接下来处sqlite3中的符号,此时意识到问题可能并不是出在XXX.obj上,而是sqlite3的库有问题,只是这里的错误信息非常具有误导性:'than other objects'只会联想到项目中的其他objects,但是它这里所指的恐怕是静态库中的objects.于是赶紧到版本库中查一下sqlite的历史,果然最近是有更新的.回滚到正确版本,问题消失。

之所以会有这个问题,在于源代码的版本和第三方库版本管理方式有问题。源代码本来是有相关联的外部库commit id的,如果都用那个id来同步代码和第三方库,就不会有问题。但是因为最近要给源码打补丁,导致源码的change list id变新,此CL id下的第三方库也是新的了,并不不兼容。解决办法是将带补丁的源码只能关联到特定时间的第三方库上。

在Debian上配置Time Machine

2017年8月27日星期日, by wingfire ; 分类: 计算机技术; 0 comments

首先创建一台lxc容器,发行版选择Debian:sid. 当然,创建lxc容器并非是必要的,但是这可以让你的宿主机器保持干净。我需要挂载宿主机上的一个目录到容器,并且想让容器自动启动,在容器的config文件中加入:

lxc.start.auto = 1
lxc.mount.entry = /path/to/host/dir mount/path  none bind 0.0

选择sid的目的是省去很多自己build软件的麻烦,直接apt安装就好了:

apt install netatalk

我在安装过程中遇到点小问题,参考lxc 中 avahi 启动失败的问题.

接下来编辑/etc/netatalk/AppleVolumes.default,删掉行~/ “Home Directory”, 加上你的备份目录:

/path/to/backup  "Disk Name" options:tm

注意,配置文件中有这么一行: :DEFAULT: options:upriv,usedots 这个upriv的意思是使用unix权限,这意味着你需要建一个unix用户,供Mac上的time machine登陆用.建好用户,设好密码,还得记得给目录/path/to/backup设好权限,以允许用户读写。我就是因为忘了给写权限,折腾了半天才找到原因.

接着,创建文件/etc/avahi/services/afpd.service,加入:

%h
_afpovertcp._tcp
548
_device-info._tcp
0
model=Xserve

然后重启服务: service netatalk restart service avahi-daemon restart

参考:

lxc 中 avahi 启动失败的问题

2017年8月27日星期日, by wingfire ; 分类: 计算机技术; 0 comments

安装Netatalk过程中,所依赖的avahi-daemon启动失败,journalctl -xe显示如下错误:

Aug 27 06:00:00 timemachine avahi-daemon[765]: Found user 'avahi' (UID 107) and group 'avahi' (GID 109).
Aug 27 06:00:00 timemachine avahi-daemon[765]: Successfully dropped root privileges.
Aug 27 06:00:00 timemachine avahi-daemon[765]: chroot.c: fork() failed: Resource temporarily unavailable
Aug 27 06:00:00 timemachine systemd[1]: avahi-daemon.service: Main process exited, code=exited, status=255/n/a
Aug 27 06:00:00 timemachine avahi-daemon[765]: failed to start chroot() helper daemon.
Aug 27 06:00:00 timemachine systemd[1]: Failed to start Avahi mDNS/DNS-SD Stack.

这个问题仅仅发生在lxc容器中,https://github.com/lxc/lxc/issues/25 对此有解释:

Ok, the reason it fails is that avahi user in the container is the same as a uid already in use on the host. avahi is very strict about setting limit for number of tasks to precisely what it wants. So in mycase, it was set to 104, which was ntp on the host, and ntpd was already running.

修正方法有二:

  • I change the container avahi's userid to 99104, did chown -R avahi /var/run/avahi-daemon (i guess no tnecessary) and rebooted. THen avahi came up.

  • One other trivial workaround is to remove 'rlimit-nproc = 3' from /etc/avahi/avahi-daemon.conf

一种Observer的实现

2017年8月24日星期四, by wingfire ; 分类: 计算机技术, 代码相关; 0 comments

Observer 模式是很常用的,但是要想实现得正确,并不容易.一个常见但并不很容易解决的问题是在Notify的过程中添加或删除Observer.

一般而言,对于一个目标,可以注册多个观察员对象,这就需要某种列表来记录,这个列表通常可以作为目标的数据成员来实现的.当需要发出通知时,代码大致如下

for(auto& v : observers)
    v.notify(event);

这段代码很简单,但是可能遇到问题,取决于notify的行为。若notify内还会注册/注销观察者,其效果就相当于在遍历的循环内修改了被遍历的容器的元素,若observers的类型是C++ 的std::vector,那将导致未定义行为。即使是std::list,若注销的恰好为当前的观察者,也会导致迭代器失效。而要想避免这种错误,实现起来并不容易。

上述错误发生的前提之一是遍历期间修改了容器,因此,如果在notify实际并不会修改容器,那就没什么问题。虽然API层面上无法保证不被误用,但可以增加一些检查加以防范:

void changed(const event &e){
  assert(!this->is_notifying);
  this->is_notifying = true;
  for(auto& v : observers)
    v.notify(e);
  this->is_notifying = false;
}

void register(observer_type o){
   assert(!this->is_notifying);
   observers.emplace_back(std::move(o));
}

void unregister(observer_type o){
   assert(!this->is_notifying);
   observers.emplace_back(std::move(o));
   observers.erase(std::remove(observers.begin(), observers.end(), o), observers.end());
}

注意,上述代码假设observer_type是可以判等的,std::function显然不满足要求.changed函数不是异常安全的,所以应该写成:

void changed(const event &e){
  assert(!this->is_notifying);
  var_saver guard(this->is_notifying, true);

  for(auto& v : observers)
    v.notify(e);
}

那如果需要允许notify内部注册注销呢?一种可行但低效的做法是,在遍历之前先复制:

void changed(const event &e){
  auto tmp = observers;
  for(auto& v : tmp)
    v.notify(e);
}

这么做的代价可能是相当昂贵的,尤其是当changed发生得非常频繁的时候.还有另外一个问题,即删除的observer不能及时得到处置,仍然可能被发送消息,这可能会带来恼人的结果.为了效率,能否既及时处置注销的观察者又不复制observers呢?

先试试看list类型,似乎简单一些:

void changed(const event &e){
  for(auto i = observers.begin(); i != observers.end(); ){
    auto& v = *i++;
    v.notify(e);
  }
}

可惜,这是错的. i虽然提前递进了,但是无法保证删的不是i所指的那一个,实际上,i无法定位到一个安全的位置上去。嗯,似乎还有办法:

void changed(const event &e){
  for(size_t i = 0; i < observers.size(); ++i){
    auto& v= observers[i];
    v.notify(e);
  }
}

这里observer的类型是vector,这段代码不会导致未定义行为.是不是没问题了?仍然不是.

如果在notify期间删除了在i之前的观察者,导致vector后面的元素都前移,这样在遍历的时候当前i后面的一个将被跳过.这种不确定性是难以被接受的.也不能通过检查observers.size()来察觉这一点,因为notify可以既删又加.

比较一下上述两种方法的得失,因为每一个observer都是特定的,下标法虽然避免了未定义行为,却丢失了observer的身份信息,iterator法的问题是虽然保住了身份信息,却没料到对象自己被删。因此修补的方法是对于下标法,要补记录观察者标志信息,对于iterator法,要将对象身份信息和对象本身脱钩。这两个方法的修订版就变得一致起来:以一种标记法标记observer,标记不依赖observer对象而存在。直观的表示法是

struct item{
   T identify;
   bool is_removed;
   observer_type* observer;
};

上面代码中的类型T应该是什么?原则上,可以是为observer分配的序号,字符串名称,或任何其他类似的东西。但是一个天然的identify就是observer的地址,因此T的类型可以取observer_type*.另外,is_removed可以通过把observer设为nullptr来表示,可以去掉。于是修改为:

struct item1{
   observer_type* identify;
   observer_type* observer;
};

这和下述表示本质上等价:

struct item2{
   bool is_removed;
   observer_type* observer;
};

对于上述定义,unregester都不立即将其从observers中删除,两者的区别在于,对于item1,把observer置为nullptr,identify不动,item2则把is_removed置为true,observer不动.于是changed可以实现为:

void changed(const event &e){
  for(size_t i = 0; i < observers.size(); ++i){
    auto& v= observers[i];
    if (!v.is_removed)
        v.observer->notify(e);
  }

  //注意: 不能在此清理observers中is_removed的项

}

实际上item1/2如果不马上删除,那本身就可以充当observer的identity,因此可以进一步简化为:

std::vector<observer_type*> observers;

void changed(const event &e){
  for(size_t i = 0; i < observers.size(); ++i){
    auto v= observers[i];
    if (v != nullptr)
        v->notify(e);
  }
}

void register(observer_type* observer){
   assert(!contains_(observer));
   observers.emplace_back(observer);
}

void unregister(observer_type* observer){
   assert(contains_(observer));
   auto pos = std::find(observers.begin(), observers.end(), observer);
   *pos = nullptr;
}

实际上,直接得到上面最后的结果也并不困难.剩下的问题是什么时候从observers中实际删除已删除的那些observer呢?这需要在相关函数中中判断一下,是否处于递归过程中,如果不处于递归,就执行实际的删除操作。那如何知道是否处于递归呢?一个简单的方法是计数。

struct counter_guard{
    int & counter;
    counter_guard(int& c) : counter(c){++counter;} 
    ~counter_guard(){--counter;}
};

int enter_counter;

void changed(const event &e){
  counter_guard guard(enter_counter);
  bool has_removed = false;
  for(size_t i = 0; i < observers.size(); ++i){
    auto v= observers[i];
    if (v != nullptr)
        v->notify(e);
    else
        has_removed = true;
  }

  if (has_removed && enter_counter == 1)
    observers.erase(std::remove(observers.begin(), observers.end(), nullptr), observers.end());

}

void register(observer_type* observer){
   assert(!contains_(observer));
   counter_guard guard(enter_counter);
   observers.emplace_back(observer);
}

void unregister(observer_type* observer){
   assert(contains_(observer));
   counter_guard guard(enter_counter);
   auto pos = std::find(observers.begin(), observers.end(), observer);
   if (enter_counter == 1)
     observers.erase(pos);   // no recursive
   else
     *pos = nullptr;    // in recursive
}

如果只在unregister中处理删除节点是不够的,因为真正导致递归的只是changed函数。相对来说changed是会被频繁调用的,因此将真正的删除放在changed中完成。要注意到上面changed中has_removed的计算并不精确,如果在迭代第i个观察者时,前面的某个被注销了,has_remove并不能感知,也就不会删除,要到下一次触发changed才能实际检测到。这样的实现是基于下述假设:

  • unregester相对发生概率低
  • 延迟做实际删除并无重大影响

以上是针对单线程情形下的分析和实现。对于多线程,则情况还要复杂得多,需要引入递归锁对执行线程进行保护。注意不能使用可升级的递归读写锁,因为递归过的顶层过程已经占有读锁,底层是不可能成功升级为排它锁的,这将导致死锁.即便锁的实现支持升级本线程的读锁为写锁,两个线程的竞争也将导致死锁无法避免.

如何让Docker容器从外部DHCP服务器获得地址并注册DNS域名

2017年2月11日星期六, by wingfire ; 分类: 计算机技术; 0 comments

我在家里的电脑上起了几个Docker容器用来运行web服务。一个是web服务器Nginx,此外还有blog、wiki等web应用,此外还跑了个MLDonkey。我本来的规划是让Nginx单独跑在一个容器中,其他的web应用都跑在各自专属的容器中,这样有利于增强安全性,维护也方便些。很遗憾,有许多困难。要解释这一点,得先解释一下Docker的网络是如何管理的。

Docker默认会创建三个网络,分别叫bridge, host 和none. 本文需要关注的是bridge网络,顾名思义,是一个桥接网络。bridge是docker默认创建的,关联到网桥设备docker0.当然可以通过docker的配置文件中指定网桥,而不是使用默认的名称。这个默认的网桥使用172.17.0.0/16网段,网关是172.17.42.1(不同机器可能会不同)。

当docker启动一个容器时,首先会创建一对虚拟以太网设备(VETH, Virtual Ethernet Device).这个veth设备总是成对创建的,把它们想象成是用一根网线连接起来的两个网口就好了。创建veth需要指定两个口的名字,docker会随机创建它们。然后docker把其中一个端口关联到docker0网桥上,相当于下面的命令:

    brctl  addif docker0 veth-xxxA

想象成是把网线插入到交换机的一个网口中就行了。

那么另一端veth-xxxB呢?不要发挥想象力,认为要插入一台主机。在一定程度上,docker容器可以被看作是虚拟机,只是这种虚拟是通过namespace的隔离来实现的。所以本质上容器内的进程和宿主机上的进程没什么不同,只是各自关联的namespace不同而已。当然,namespace上施加的限制也不同。关于namespace,可以参考namespaces - overview of Linux PID namespaces。为了把veth-xxxB个容器关联起来,docker会把veth-xxxB移到容器所在的network namespace中,并改名成eth0.于是,在容器中就能看到eth0设备了,但实际上和宿主机的eth0虽然同名,但并没有什么关系。接下来,docker会给veth-xxxB分配ip地址--当然还包括路由设置,ip是从配置好的地址池中选择的。更详细的关于地址分配的工作流程,我还没有在文档中看到。

一般来说,分属于不同网络空间的设备是不能直接通信的,但veth设备对是个例外。veth设备对实际上是个管道,数据从任意一头进去,就会从另一头被推出来。因此虽然分属不同的网络空间,但是Linux内核仍然允许两者通信。这样,从容器中经eth0发出的数据包会被送入veth-xxxA, 从而从veth-xxxB中出来,进入到关联的docker0.但是docker0和物理网卡eth0之间并没有连接起来,因此,以太数据包到此就结束了,没法传到物理网络上去的。如果我们希望docker0是一个和外部隔绝的网络,这恰好就是我们想要的。可是,如果要和外部网络通信怎么办?这时候就需要路由器的帮助了。docker0和eth0此时都是在宿主机的名字空间可见的,可以通过iptables设置nat实现docker0和宿主机eth0的通信。

来看我的情况。Nginx提供外网服务,是要能够从外面访问容器的80端口的,可是容器在Nat后面啊,那就只能在Nat上打洞,开端口映射了。Docker允许开端口映射,80和443嘛,又不麻烦。blog和wiki,是Nginx去访问,因此通信只局限在docker0之内,并不需要在nat上做端口映射。

第一个问题来了。我在Nginx中如何设置upstream到后端的blog和wiki呢?每个容器的ip是动态分配的,我在写Nginx配置的时候并不能确定。常规的Nginx配置并没有这个问题,因为后端要么有域名,要么有静态ip。静态ip不太讨人喜欢,域名最好。但是docker并不自带dhcp和域名服务啊,怎么办?理想的方案是用传统的dhcp和域名系统进行管理。

第二个问题,MLDonkey要开许多监听,于是我不得不做很多的端口映射。这只是一个容器。要是我多开几个呢?光端口映射就得烦死我。另外一个,如果我想尝试起两个Nginx的容器,那我是没办法把一个80端口同时映射到两个容器的。况且,我运行Docker的机器本来就是在内部网络,为什么还需要做一次Nat才能到内网呢?它们应该完全可以工作得就像实际存在的若干台物理主机啊,这只需要把网桥docker0和eth0关联到一起就足以解决了。所以,理想的方案是容器直接从外部网络系统获取ip地址,通过二层交换和外网通信,而不是走三层的Nat。这里所谓的“外部”网络,并不一定要是真实的物理网络,也可以是虚拟的,但是是独立于docker和docker所在宿主机之外的。毕竟,很多docker的宿主机其实也只是个虚拟机而已。

我不太认同Docker以及一些容器管理软件做的网络管理功能,为什么不利用已有的网络管理技术呢?还是说你发现了它们都有某种缺陷?还是你做得比它们好?现状是Docker做的不好,许多管理软件做得也不好,太复杂不说还有种种局限。对于网络,Docker只要做好容器的必要支持,使得传统的网络技术得以延伸到容器上,就好了。如果容器有什么网络需求是传统网络管理处理不了的,Docker你去为这些传统网络扩展管理工具就好。

对于上面两个问题,我很长时间以来都没什么进展。最近新装了台机器,决定死磕,终于找到了解决方案。

首先,docker0是Docker管理的,我不可能真的把宿主机eth0绑定到它上面去。我大致考虑了一下网络拓扑,具体如下:

DockerBridge

中间这个网桥不使用docker0,而是专门创建一个网桥设备似乎更合理,只是我后来发现使用docker0也并没有什么问题,简单起见,就这样。红色的eth0设备是连接到网桥br0上。用下面的命令来创建br0, 注意,过程中网络会中断,别远程操作。

# brctl addbr br0
# brctl addbr docker0 
# ip link add veth-br0 type veth peer name veth-docker0
# brctl addif br0 veth-br0
# brctl addif br0 eth0
# brctl addif docker0 veth-docker0
# ip link set br0 up
# ip link set eth0 up
# dhclient -v br0
# ip link set veth-br0 up
# ip link set veth-docker0 up

也可以修改/etc/network/interfaces:

    auto lo br0

    iface veth-br0 inet manual
        up ip link add veth-br0 type veth peer name veth-docker0
        down ip link delete veth-br0

    iface br0 inet dhcp
        pre-up ifup eth0
        post-down idown eth0
        bridge_ports eth0 veth-br0
        bridge_stp off

    iface docker0 inet auto
        pre-up ifup veth-br0
        post-down ifdown veth-br0
        bridge_ports veth-docker0

上面的配置并没有给docker0分配地址,但是docker并不会搭理你,在启动后任然会给docker0分配一个ip。这个时候,假设你起了个容器,hostname设为test,注意不要和host机器的相同:

    docker run -ti --rm --hostname test --name test debian:latest /bin/bash

在容器里查看地址:

    ip addr show eth0

你会看到docker还是给这个容器分配了一个172开头的IP地址,没有走dhcp啊!别急,先切回host,在root下运行下面的命令:

    # hostname
    # pid=$(docker inspect -f '{{.State.Pid}}' test)
    # nsenter -t $pid -u -n
    # hostname

对比前后两个hostname的输出,第二个输出应该是test。nsenter会启动一个shell,并且关联了test容器的uts和network名字空间。下面的命令都是在nsenter的shell里运行的,先检查一下:

    # ip addr show             # 此时只会列出lo和eth0
    # ip route list                # 默认网关时指向172开头的docker0的ip

好,开始dhcp请求ip地址:

    # ip addr flush dev eth0     # 删除docker分配的IP地址.
    # dhclient -v eth0

如果前面的配置没有错误的话,这时就能看到dhcp成功分配的ip地址了。在宿主机上运行nslookup test就可以验证是否成功注册了DNS域名。对于Debian系统,可以检查一下/etc/dhcp/dhclient.conf,是否写了send host-name = gethostname();。脚本的github地址

遗留问题:

dhclient被调用后会一直在后台运行,即使容器退出了,dhclient进程也不会结束。这对有系统洁癖的人来说可能有点难受。

好了,说说的我的感受。

Docker至今还是不能通过命令行修改容器配置。网上有些人说可以直接改配置文件的,太naive了。docker服务在停止时,会保存所有容器配置。也就是说,如果你不先停下docker,更新的配置也会被覆盖掉。而要停下docker,那所有的容器都会终止运行。这个对于经常要在docker里面搞开发,装这个装那个的人来说,太麻烦了。如果说配置很难做到在线改,那就只要求被修改的容器必须先停止运行,这样的限制也比要停止docker服务好得多。再说了,docker network咋就允许在线修改了呢?

我的工作和容器其实没什么关系,就是自己折腾,因此k8s啊,swarm啊我都没摸过。但是我实在不觉得一个基本网络管理这样的小事非要劳驾一些重量级软件,而且这些软件的解决手法也是让人看得皱眉,除了pipework大概算是个例外。我对Docker下一个期待的特性是热迁移,希望工具能足够简单。

参考:

  1. https://github.com/jpetazzo/pipework

  2. https://docs.docker.com/v1.5/articles/networking/#bridge-building

previous page

下一页