月度归档:2015年07月

收集的一些nginx,apache测试参数

本文摘取自以下文件的局部内容:http://zyan.cc/post/366/

 

在高并发连接的情况下,Nginx是Apache服务器不错的替代品。Nginx同时也可以作为7层负载均衡服务器来使用。根据我的测试结果,Nginx 0.7.51 + PHP 5.2.8 (FastCGI) 可以承受3万以上的并发连接数,相当于同等环境下Apache的10倍。

  根据我的经验,4GB内存的服务器+Apache(prefork模式)一般只能处理3000个并发连接,因为它们将占用3GB以上的内存,还得为系统预留1GB的内存。我曾经就有两台Apache服务器,因为在配置文件中设置的MaxClients为4000,当Apache并发连接数达到3800时,导致服务器内存和Swap空间用满而崩溃。

  而这台 Nginx 0.7.51 + PHP 5.2.8 (FastCGI) 服务器在3万并发连接下,开启的10个Nginx进程消耗150M内存(15M*10=150M),开启的64个php-cgi进程消耗1280M内存(20M*64=1280M),加上系统自身消耗的内存,总共消耗不到2GB内存。如果服务器内存较小,完全可以只开启25个php-cgi进程,这样php-cgi消耗的总内存数才500M。

  在3万并发连接下,访问Nginx 0.7.51 + PHP 5.2.8 (FastCGI) 服务器的PHP程序,仍然速度飞快。下图为Nginx的状态监控页面,显示的活动连接数为28457(关于Nginx的监控页配置,会在本文接下来所给出的Nginx配置文件中写明):

  点击在新窗口中浏览此图片

点击查看原图

  我生产环境下的两台Nginx + PHP5(FastCGI)服务器,跑多个一般复杂的纯PHP动态程序,单台Nginx + PHP5(FastCGI)服务器跑PHP动态程序的处理能力已经超过“700次请求/秒”,相当于每天可以承受6000万(700*60*60*24=60480000)的访问量(更多信息见此),而服务器的系统负载也不高:

  点击在新窗口中浏览此图片

点击查看原图

FastDFS目录及文件名的一些资料

根据网上的资料,一般单目录下的文件个数一般限制不能够超过3万;同样的,一个目录下面的目录数也最好不要超过这个数。

但实际上,为了安全考虑,一般都不要存储这么多的内容。

假定,一个目录下面,存储1000个文件,每个文件的平均大小为10KB,则单目录下面可存储的容量是10MB。这个容量太小了,所以我们要多个目录,假定有1000个目录,每个目录存储10MB,则可以存储10GB的内容;这对于目前磁盘的容量来说,利用率还是不够的。再想办法,转成两级目录,这样的话,就是第一层目录有1000个子目录,每一级子目录下面又有1000级的二级子目录,每个二级子目录,可以存储10MB的内容,此时就可以存储10T的内容,这基本上超过了目前单机磁盘的容量大小了。

所以,使用二级子目录的办法,是平衡存储性能和利用存储容量的办法。

 

 

 

 

 

服务器收到了文件内容后,如何选择要存储在哪个目录下呢?这个选择要保证均衡性,即尽量保证文件能够均匀地分散在所有的目录下。

负载均衡性很重要的就是哈希,例如,在PHP中常用的md5,其返回一个32个字符,即16字节的输出,即128位。哈希后要变成桶,才能够分布,自然就有了如下的问题:

1- 如何得到哈希值?md5还是SHA1

2- 哈希值得到后,如何构造哈希桶

3- 根据文件名称如何定位哈希桶

首先来回答第3个问题,根据文件名称如何定位哈希桶。很简单,此时我们只有一个文件名称作为输入,首先要计算哈希值,只有一个办法了,就是根据文件名称来得到哈希值。这个函数可以用整个文件名称作为哈希的输入,也可以根据文件名称的一部分来完成。结合上面说的两级目录,而且每级目录不要超过1000.很简单,如果用32位的字符输出后,可以取出实现上来说,由于文件上传是防止唯一性,所以如果根据文件内容来产生哈希,则比较好的办法就是截取其中的4位,例如:

md5sum fdfs_storaged.pid

52edc4a5890adc59cec82cb60f8af691 fdfs_storaged.pid

上面,这个fdfs_storage.pid中,取出最前面的4个字符,即52和ed。这样的话,假如52是一级目录的名称,ed是二级目录的名称。因为每一个字符有16个取值,所以第一级目录就有16 * 16 = 256个。总共就有256 * 256 = 65526个目录。如果每个目录下面存放1000个文件,每个文件30KB,都可以有1966G,即2TB左右。这样的话,足够我们用好。如果用三个字符,即52e作为一级目录,dc4作为二级目录,这样子的目录数有4096,太多了。所以,取二个字符比较好。

这样的话,上面的第2和第3个问题就解决了,根据文件名称来得到md5,然后取4个字符,前面的2个字符作为一级目录名称,后面的2个字符作为二级目录的名称。服务器上,使用一个专门的目录来作为我们的存储根目录,然后下面建立这么多子目录,自然就很简单了。

这些目录可以在初始化的时候创建出来,而不用在存储文件的时候才建立。

也许你会问,一个目录应该不够吧,实际上很多的廉价机器一般都配置2块硬盘,一块是操作系统盘,一块是数据盘。然后这个数据盘挂在一个目录下面,以这个目录作为我们的存储根目录就好了。这样也可以很大程度上减少运维的难度。

现在就剩下最后一个问题了,就是上传文件时候,如何分配一个唯一的文件名称,避免同以前的文件产生覆盖。

如果没有变量作为输入,很显然,只能够采用类似于计数器的方式,即一个counter,每次加一个文件就增量。但这样的方式会要求维护一个持久化的counter,这样比较麻烦。最好不要有历史状态的纪录。

string md5 ( string $str [, bool $raw_output = false ] )

Calculates the MD5 hash of str using the » RSA Data Security, Inc. MD5 Message-Digest Algorithm, and returns that hash.

raw_output

If the optional raw_output is set to TRUE, then the md5 digest is instead returned in raw binary format with a length of 16.

Return Values ¶

Returns the hash as a 32-character hexadecimal number.

为了尽可能地生成唯一的文件名称,可以使用文件长度(假如是100MB的话,相应的整型可能会是4个字节,即不超过2^32, 即uint32_t,只要程序代码中检查一下即可)。但是长度并不能够保证唯一,为了填充尽可能有用的信息,CRC32也是很重要的,这样下载程序后,不用做额外的交互就可以知道文件的内容是否正确。一旦发现有问题,立马要报警,并且想办法修复。这样的话,上传的时候也要注意带上CRC32,以防止在网络传输和实际的硬盘存储过程中出现问题(文件的完整性至关重要)。再加上时间戳,即long型的64位,8个字节。最后再加上计数器,因为这个计数器由storage提供,这样的话,整个结构就是:len + crc32 + timestamp + uint32_t = 4 + 4 + 8 + 4 = 20个字节,这样生成的文件名就算做base64计算出来,也就不是什么大问题了。而且,加上计数器,每秒内只要单机不上传超过1万的文件 ,就都不是问题了。这个还是非常好解决的

 

// TODO 如何避免文件重复上传? md5吗? 还是文件的计算可以避免此问题?这个信息存储在tracker服务器中吗?

FastDFS中给我们一个非常好的例子,请参考下面的代码:

// 参考FastDFS的文件名称生成算法

/**
1 byte: store path index
8 bytes: file size
FDFS_FILE_EXT_NAME_MAX_LEN bytes: file ext name, do not include dot (.)
file size bytes: file content
**/
static int storage_upload_file(struct fast_task_info *pTask, bool bAppenderFile)
{
 StorageClientInfo *pClientInfo;
 StorageFileContext *pFileContext;
 DisconnectCleanFunc clean_func;
 char *p;
 char filename[128];
 char file_ext_name[FDFS_FILE_PREFIX_MAX_LEN + 1];
 int64_t nInPackLen;
 int64_t file_offset;
 int64_t file_bytes;
 int crc32;
 int store_path_index;
 int result;
 int filename_len;
 pClientInfo = (StorageClientInfo *)pTask->arg;
 pFileContext = &(pClientInfo->file_context);
 nInPackLen = pClientInfo->total_length - sizeof(TrackerHeader);
 if (nInPackLen < 1 + FDFS_PROTO_PKG_LEN_SIZE +
   FDFS_FILE_EXT_NAME_MAX_LEN)
 {
  logError("file: "__FILE__", line: %d, " \
   "cmd=%d, client ip: %s, package size " \
   "%"PRId64" is not correct, " \
   "expect length >= %d", __LINE__, \
   STORAGE_PROTO_CMD_UPLOAD_FILE, \
   pTask->client_ip, nInPackLen, \
   1 + FDFS_PROTO_PKG_LEN_SIZE + \
   FDFS_FILE_EXT_NAME_MAX_LEN);
  return EINVAL;
 }
 p = pTask->data + sizeof(TrackerHeader);
 store_path_index = *p++;
 if (store_path_index == -1)
 {
  if ((result=storage_get_storage_path_index( \
   &store_path_index)) != 0)
  {
   logError("file: "__FILE__", line: %d, " \
    "get_storage_path_index fail, " \
    "errno: %d, error info: %s", __LINE__, \
    result, STRERROR(result));
   return result;
  }
 }
 else if (store_path_index < 0 || store_path_index >= \
  g_fdfs_store_paths.count)
 {
  logError("file: "__FILE__", line: %d, " \
   "client ip: %s, store_path_index: %d " \
   "is invalid", __LINE__, \
   pTask->client_ip, store_path_index);
  return EINVAL;
 }
 file_bytes = buff2long(p);
 p += FDFS_PROTO_PKG_LEN_SIZE;
 if (file_bytes < 0 || file_bytes != nInPackLen - \
   (1 + FDFS_PROTO_PKG_LEN_SIZE + \
    FDFS_FILE_EXT_NAME_MAX_LEN))
 {
  logError("file: "__FILE__", line: %d, " \
   "client ip: %s, pkg length is not correct, " \
   "invalid file bytes: %"PRId64 \
   ", total body length: %"PRId64, \
   __LINE__, pTask->client_ip, file_bytes, nInPackLen);
  return EINVAL;
 }
 memcpy(file_ext_name, p, FDFS_FILE_EXT_NAME_MAX_LEN);
 *(file_ext_name + FDFS_FILE_EXT_NAME_MAX_LEN) = '\0';
 p += FDFS_FILE_EXT_NAME_MAX_LEN;
 if ((result=fdfs_validate_filename(file_ext_name)) != 0)
 {
  logError("file: "__FILE__", line: %d, " \
   "client ip: %s, file_ext_name: %s " \
   "is invalid!", __LINE__, \
   pTask->client_ip, file_ext_name);
  return result;
 }
 pFileContext->calc_crc32 = true;
 pFileContext->calc_file_hash = g_check_file_duplicate;
 pFileContext->extra_info.upload.start_time = g_current_time;
 strcpy(pFileContext->extra_info.upload.file_ext_name, file_ext_name);
 storage_format_ext_name(file_ext_name, \
   pFileContext->extra_info.upload.formatted_ext_name);
 pFileContext->extra_info.upload.trunk_info.path. \
    store_path_index = store_path_index;
 pFileContext->extra_info.upload.file_type = _FILE_TYPE_REGULAR;
 pFileContext->sync_flag = STORAGE_OP_TYPE_SOURCE_CREATE_FILE;
 pFileContext->timestamp2log = pFileContext->extra_info.upload.start_time;
 pFileContext->op = FDFS_STORAGE_FILE_OP_WRITE;
 if (bAppenderFile)
 {
  pFileContext->extra_info.upload.file_type |= \
     _FILE_TYPE_APPENDER;
 }
 else
 {
  if (g_if_use_trunk_file && trunk_check_size( \
   TRUNK_CALC_SIZE(file_bytes)))
  {
   pFileContext->extra_info.upload.file_type |= \
      _FILE_TYPE_TRUNK;
  }
 }
 if (pFileContext->extra_info.upload.file_type & _FILE_TYPE_TRUNK)
 {
  FDFSTrunkFullInfo *pTrunkInfo;
  pFileContext->extra_info.upload.if_sub_path_alloced = true;
  pTrunkInfo = &(pFileContext->extra_info.upload.trunk_info);
  if ((result=trunk_client_trunk_alloc_space( \
   TRUNK_CALC_SIZE(file_bytes), pTrunkInfo)) != 0)
  {
   return result;
  }
  clean_func = dio_trunk_write_finish_clean_up;
  file_offset = TRUNK_FILE_START_OFFSET((*pTrunkInfo));
    pFileContext->extra_info.upload.if_gen_filename = true;
  trunk_get_full_filename(pTrunkInfo, pFileContext->filename, \
    sizeof(pFileContext->filename));
  pFileContext->extra_info.upload.before_open_callback = \
     dio_check_trunk_file_when_upload;
  pFileContext->extra_info.upload.before_close_callback = \
     dio_write_chunk_header;
  pFileContext->open_flags = O_RDWR | g_extra_open_file_flags;
 }
 else
 {
  char reserved_space_str[32];
  if (!storage_check_reserved_space_path(g_path_space_list \
   [store_path_index].total_mb, g_path_space_list \
   [store_path_index].free_mb - (file_bytes/FDFS_ONE_MB), \
   g_avg_storage_reserved_mb))
  {
   logError("file: "__FILE__", line: %d, " \
    "no space to upload file, "
    "free space: %d MB is too small, file bytes: " \
    "%"PRId64", reserved space: %s", \
    __LINE__, g_path_space_list[store_path_index].\
    free_mb, file_bytes, \
    fdfs_storage_reserved_space_to_string_ex( \
      g_storage_reserved_space.flag, \
          g_avg_storage_reserved_mb, \
      g_path_space_list[store_path_index]. \
      total_mb, g_storage_reserved_space.rs.ratio,\
      reserved_space_str));
   return ENOSPC;
  }
  crc32 = rand();
  *filename = '\0';
  filename_len = 0;
  pFileContext->extra_info.upload.if_sub_path_alloced = false;
  if ((result=storage_get_filename(pClientInfo, \
   pFileContext->extra_info.upload.start_time, \
   file_bytes, crc32, pFileContext->extra_info.upload.\
   formatted_ext_name, filename, &filename_len, \
   pFileContext->filename)) != 0)
  {
   return result;
  }
  clean_func = dio_write_finish_clean_up;
  file_offset = 0;
    pFileContext->extra_info.upload.if_gen_filename = true;
  pFileContext->extra_info.upload.before_open_callback = NULL;
  pFileContext->extra_info.upload.before_close_callback = NULL;
  pFileContext->open_flags = O_WRONLY | O_CREAT | O_TRUNC \
      | g_extra_open_file_flags;
 }
  return storage_write_to_file(pTask, file_offset, file_bytes, \
   p - pTask->data, dio_write_file, \
   storage_upload_file_done_callback, \
   clean_func, store_path_index);
}
 
static int storage_get_filename(StorageClientInfo *pClientInfo, \
 const int start_time, const int64_t file_size, const int crc32, \
 const char *szFormattedExt, char *filename, \
 int *filename_len, char *full_filename)
{
 int i;
 int result;
 int store_path_index;
 store_path_index = pClientInfo->file_context.extra_info.upload.
    trunk_info.path.store_path_index;
 for (i=0; i<10; i++)
 {
  if ((result=storage_gen_filename(pClientInfo, file_size, \
   crc32, szFormattedExt, FDFS_FILE_EXT_NAME_MAX_LEN+1, \
   start_time, filename, filename_len)) != 0)
  {
   return result;
  }
  sprintf(full_filename, "%s/data/%s", \
   g_fdfs_store_paths.paths[store_path_index], filename);
  if (!fileExists(full_filename))
  {
   break;
  }
  *full_filename = '\0';
 }
 if (*full_filename == '\0')
 {
  logError("file: "__FILE__", line: %d, " \
   "Can't generate uniq filename", __LINE__);
  *filename = '\0';
  *filename_len = 0;
  return ENOENT;
 }
 return 0;
}
static int storage_gen_filename(StorageClientInfo *pClientInfo, \
  const int64_t file_size, const int crc32, \
  const char *szFormattedExt, const int ext_name_len, \
  const time_t timestamp, char *filename, int *filename_len)
{
 char buff[sizeof(int) * 5];
 char encoded[sizeof(int) * 8 + 1];
 int len;
 int64_t masked_file_size;
 FDFSTrunkFullInfo *pTrunkInfo;
 pTrunkInfo = &(pClientInfo->file_context.extra_info.upload.trunk_info);
 int2buff(htonl(g_server_id_in_filename), buff);
 int2buff(timestamp, buff+sizeof(int));
 if ((file_size >> 32) != 0)
 {
  masked_file_size = file_size;
 }
 else
 {
  COMBINE_RAND_FILE_SIZE(file_size, masked_file_size);
 }
 long2buff(masked_file_size, buff+sizeof(int)*2);
 int2buff(crc32, buff+sizeof(int)*4);
 base64_encode_ex(&g_fdfs_base64_context, buff, sizeof(int) * 5, encoded, \
   filename_len, false);
 if (!pClientInfo->file_context.extra_info.upload.if_sub_path_alloced)
 {
  int sub_path_high;
  int sub_path_low;
  storage_get_store_path(encoded, *filename_len, \
   &sub_path_high, &sub_path_low);
  pTrunkInfo->path.sub_path_high = sub_path_high;
  pTrunkInfo->path.sub_path_low = sub_path_low;
  pClientInfo->file_context.extra_info.upload. \
    if_sub_path_alloced = true;
 }
 len = sprintf(filename, FDFS_STORAGE_DATA_DIR_FORMAT"/" \
   FDFS_STORAGE_DATA_DIR_FORMAT"/", \
   pTrunkInfo->path.sub_path_high,
   pTrunkInfo->path.sub_path_low);
 memcpy(filename+len, encoded, *filename_len);
 memcpy(filename+len+(*filename_len), szFormattedExt, ext_name_len);
 *filename_len += len + ext_name_len;
 *(filename + (*filename_len)) = '\0';
 return 0;
}



 

根据上面分析的结果,我们看到,当上传一个文件的时候,我们会获取到如下的信息

1- 文件的大小(通过协议中包的长度字段可以知道,这样的好处在于服务端实现的时候简单,不用过于担心网络缓冲区的问题)

2- CRC32(也是协议包中传输,以便确定网络传输是否出错)

3- 时间戳(获取服务器的当前时间)

4- 计数器(服务器自己维护)

根据上面的4个数据,组织成base64的编码,然后生成此文件名称。根据此文件名称的唯一性,就不会出现被覆盖的情况。同时,唯一性也使得接下来做md5运算后,得到的HASH值离散性得么保证。得到了MD5的哈希值后,取出最前面的2部分,就可以知道要定位到哪个目录下面去。哈希桶的构造是固定的,即二级00-ff的目录情况

ruby gem -v yaml

gem –version

如果在使用gem的时候碰到这样的提示:

gem --version# /usr/local/lib/ruby/1.9.1/yaml.rb:84:in `<top (required)>': # It seems your ruby installation is missing psych (for YAML output). # To eliminate this warning, please install libyaml and reinstall your ruby. 

这是在编译安装 ruby 的时候没有先安装 libyaml 所致。但是,即使是你安装 libyaml 之后重新安装 ruby ,这个问题还是不能解决。

正确的方法,是安装 libyam-devel 库。下面是在 CentOS 6.3 上安装:

yum install libyaml-devel

然后,找到你先前编译 ruby 的目录,进入 ext/psych/ 文件夹,执行:

ruby extconf.rb make make install# checking for yaml.h... yes # checking for yaml_get_version() in -lyaml... yes # creating Makefile 

然后再执行一次 make install 。如果你已经 clean 了原来的编译内容,那么则需要重新编译。

make install# compiling to_ruby.c # compiling parser.c # compiling psych.c # compiling emitter.c # compiling yaml_tree.c # linking shared-object psych.so # /usr/bin/install -c -m 0755 psych.so /usr/local/lib/ruby/site_ruby/1.9.1/x86_64-linux # installing default psych libraries 

再次执行 gem -v ,发现 warning 已经消失了。

Twitter 高并发高可用架构

解决 Twitter的“问题”就像玩玩具一样,这是一个很有趣的扩展性比喻。每个人都觉得 Twitter很简单,一个菜鸟架构师随便摆弄一下个可伸缩的 Twitter就有了,就这么简单。然而事实不是这样, Twitter的工程副总裁 Raffi Krikorian细致深入的描述了在 Twitter在可伸缩性上的演化过程,如果你想知道 Twitter的如何工作—从这里开始吧。

Twitter发展太快,一切转瞬即过,但 Twitter已经长大了。它从一开始一个在Ruby on Rails上苦苦挣扎的小网站变成一个以服务为 核心驱动的漂亮站点,服务停掉都难得一见,很大的一个转变。

Twitter现在有1.5亿全球活跃用户,300K QPS,22 MB/秒的流量,系统每天处理4亿条推特数据,用5分钟时间将Lady Gaga手尖流淌出的信息传递到她3100万个关注者。

一些需要列出来的要点:

  • Twitter不再希望成为一个Web应用程序,Twitter想要成为一套驱动全世界手机客户端的API,作为地球上最大的实时交互工具。
  • Twitter主要在分发消息,而不是生产消息,300K QPS是在读而每秒仅有6000请求在写。
  • 不对称,有数量庞大的粉丝,现在正成为一种常见情况。单个用户发送的消息有很多追随者要看到,这是一个大的扇型输出,分发可能很缓慢,Twitter试图保证在5秒以内,但并不能总是达到这个目标,尤其是当名人或名人之间发推特时,这种情况越来越常见,一个可能后果是在还未看到原始消息之前接受到了回复。Twitter做工作是在迎接高关注度用户所写推特读取的挑战。
  • 你主页的数据储存在由800多个Redis节点组成的集群上。
  • 从你关注的人及你点击的链接Twitter更了解你。可以通过隐私设置的双向以下时不存在。
  • 用户关心推特内容本身,但对Twitter而言推特的内容与其基础设施建设几乎无关。
  • 需要一个非常复杂的监控和调试系统来跟踪复杂协议栈内的性能问题。传统的遗留问题一直困扰着系统。

Twitter是如何工作的?通过Raffi精彩的演讲来发现吧…

面临的挑战

  • 可靠的实现150万在线用户及300K QPS(主页和搜索访问),响应慢怎么办?
  • 可靠的实现是一个对所有推特的select语句,响应忙死卡死。
  • 数据扇形输出的解决方案。当接收到新推特时需要弄清楚应该把它发到哪,这样可以更快速简单的读取,不要在读操作上做任何逻辑计算,性能上写要比读慢得多,能做到4000 QPS。

内部组成

  • 平台服务部门负责Twitter的核心基础设施的可扩展性。
    • 他们运行的东西为时间轴、微博、用户及社交网络提供服务,包括所有支撑Twitter平台的系统设备。
    • 统一内外部客户使用相同的API。
    • 为数百万的第三方应用注册支持
    • 支持产品团队,让他们专注产品无系统支撑方面顾虑。
    • 致力于容量规划、构建可扩展的后端系统等工作,通过不断更换设施使网站达到意想不到的效果。
  • Twitter有一个架构师部门,负责Twitter整体架构,研究技术改进路线(他们想一直走在前面)。

Push、Pull模式

  • 每时每刻都有用户在Twitter上发表内容,Twitter工作是规划如何组织内容并把它发送用户的粉丝。
  • 实时是真正的挑战,5秒内将消息呈现给粉丝是现阶段的目标。
    • 投递意味着内容、投入互联网,然后尽可能快的发送接收。
    • 投递将历时数据放入存储栈,推送通知,触发电子邮件,iOS、黑莓及Android手机都能被通知到,还有短信。
    • Twitter是世界上活跃中最大的信息发送机。
    • 推荐是内容产生并快速传播的巨大动力。
  • 两种主要的时间轴:用户的及主页的。
    • 用户的时间轴特定用户发送的内容。
    • 主页时间表是一段时间内所有你关注用户发布的内容。
    • 线上规则是这样的:@别人是若被@的人你未关注的话将被隔离出来,回复一个转发可以被过滤掉。
    • 这样在Twitter对系统是个挑战。
  • Pull模式
    • 有针对性的时间轴。像twitter.com主页和home_timeline的API。你请求它才会得到数据。拉请求的不少:通过REST API请求从Twitter获取数据。
    • 查询时间轴,搜索的API。查询并尽可能快的返回所有匹配的推特。
  • Push模式
    • Twitter运行着一个最大的实时事件系统,出口带宽22MB/秒。
      • 和Twitter建立一个连接,它将把150毫秒内的所有消息推送给你。
      • 几乎任何时候,Push服务簇上大约有一百万个连接。
      • 像搜索一样往出口发送,所有公共消息都通过这种方式发送。
      • 不,你搞不定。(实际上处理不了那么多)
    • 用户流连接。 TweetDeck 和Twitter的Mac版都经过这里。登录的时,Twitter会查看你的社交图,只会推送那些你关注的人的消息,重建主页时间轴,而不是在持久的连接过程中使用同一个时间轴 。
    • 查询API,Twitter收到持续查询时,如果有新的推特发布并且符合查询条件,系统才会将这条推特发给相应的连接。

高观点下的基于Pull(拉取方式)的时间轴:

  • 短消息(Tweet)通过一个写API传递进来。通过负载平衡以及一个TFE(短消息前段),以及一些其它的没有被提到的设施。
  • 这是一条非常直接的路径。完全预先计算主页的时间轴。所有的业务逻辑在短消息进入的时候就已经被执行了。
  • 紧接着扇出(向外发送短消息)过程开始处理。进来的短消息被放置到大量的Redis集群上面。每个短息下在三个不同的机器上被复制3份。在Twitter 每天有大量的机器故障发生。
  • 扇出查询基于Flock的社交图服务。Flock 维护着关注和被关注列表。
  • Flock 返回一个社交图给接受者,接着开始遍历所有存储在Redis 集群中的时间轴。
  • Redis 集群拥有若干T的内存。
  • 同时连接4K的目的地。
  • 在Redis 中使用原生的链表结构。
  • 假设你发出一条短消息,并且你有20K个粉丝。扇出后台进程要做的就是在Redis 集群中找出这20K用户的位置。接着它开始将短消息的ID 注入到所有这些列表中。因此对于每次写一个短消息,都有跨整个Redis集群的20K次的写入操作。
  • 存储的是短消息的ID, 最初短消息的用户ID, 以及4个字节,标识这条短消息是重发还是回复还是其它什么东东。
  • 你的主页的时间轴驻扎在Redis集群中,有800条记录长。如果你向后翻很多页,你将会达到上限。内存是限制资源决定你当前的短消息集合可以多长。
  • 每个活跃用户都存储在内存中,用于降低延迟。
  • 活跃用户是在最近30天内登陆的twitter用户,这个标准会根据twitter的缓存的使用情况而改变。
  • 只有你主页的时间轴会存储到磁盘上。
  • 如果你在Redis 集群上失败了,你将会进入一个叫做重新构建的流程。
  •      查新社交图服务。找出你关注的人。对每一个人查询磁盘,将它们放入Redis中。
  •      MySQL通过Gizzard 处理磁盘存储,Gizzard 将SQL事务抽象出来,提供了全局复制。
  • 通过复制3次,当一台机器遇到问题,不需要在每个数据中心重新构建那台机器上的时间轴。
  • 如果一条短消息是另外一条的转发,那么一个指向原始短消息的指针将会存储下来。
  • 当你查询你主页的时间轴时候,时间轴服务将会被查询。时间轴服务只会找到一台你的时间轴所在的机器。
  •      高效的运行3个不同的哈希环,因为你的时间轴存储在3个地方。
  •      它们找到最快的第一个,并且以最快速度返回。
  •      需要做的妥协就是,扇出将会花费更多的时间,但是读取流程很快。大概从冷缓存到浏览器有2秒种时间。对于一个API调用,大概400ms。
  • 因为时间轴只包含短消息ID, 它们必须”合成”这些短消息,找到这些短消息的文本。因为一组ID可以做一个多重获取,可以并行地从T-bird 中获取短消息。
  • Gizmoduck 是用户服务,Tweetypie 是短消息对象服务。每个服务都有自己的缓存。用户缓存是一个memcache集群 拥有所有用户的基础信息。Tweetypie将大概最近一个半月的短消息存储在memcache集群中。这些暴露给内部的用户。
  • 在边界将会有一些读时过滤。例如,在法国过滤掉纳粹内容,因此在发送之前,有读时内容剥离工作。

高级搜索

  • 与Pull相反,所有计算都在读时执行,这样可以使写更简单。
  • 产生一条推特时,Ingester会对其做语法分析找出新建索引的一切东西,然后将其传入一台Early Bird机器。Early Bird是Lucene的修改版本,索引储存在内存中。
  • 在推特的分发过程中可能被储存在多个由粉丝数量决定的主页时间轴中,一条推特只会存入一个Early Bird机器中(不包括备份)。
  • Blender进行时间轴的跨数据中心集散查询。为发现与查询条件匹配的内容它查询每个Early Bird。如果你搜索“纽约时报”,所有分片将被查询,结果返回后还会做分类、合并及重排序等。排序是基于社会化度量的,这些度量基于转发、收藏及评论的数量等。
  • 互动信息是在写上完成的,这里会建立一个互动时间轴。当收藏或回复一条推特时会触发对互动时间轴的修改,类似于主页时间轴,它是一系列的活跃的ID,有最受喜欢的ID,新回复ID等。
  • 所有这些都被送到Blender,在读路径上进行重计算、合并及分类,返回的结果就是搜索时间轴上看到的东西。
  • Discovery是基于你相关信息的定制搜索,Twitter通过你关注的人、打开的链接了解你的信息,这些信息被应用在Discovery搜索上,重新排序也基于这些信息。

Search和Pull是相反的

  • Search和Pull明显的看起来很相似,但是他们在某些属性上却是相反的。
  • 在home timeline时:
    • 写操作。tweet写操作引发O(n)个进程写入Redis集群,n代表你的粉丝,如果是这样,处理Lady Gaga或是Obama百万粉丝的数据就得用10s的时间,那是很难接受的。所有的Redis集群都支持硬盘处理数据,但是一般都是在RAM里操作的。
    • 读操作。通过API或是网络可用O(1)的时间来找到Redis机器。Twitter在寻找主页路径方面做了大量的优化。读操作可在10毫秒完成。所有说Twitter主导消费机制而不是生产机制。每秒可处理30万个请求和6000 RPS写操作。
  • 在搜索timeline时:
    • 写操作。一个tweet请求由Ingester收到并有一个Early Bird机器来处理。写操作时O(1).一个tweet需要5秒的处理时间,其包括排队等待和寻找路径。
    • 读操作。读操作引发O(n)个集群读操作。大多数人不用search,这样他们可以在存储tweets上面更加有效,但是他们得花时间。读需要100毫秒,search不涉及硬盘操作。全部Lucene 索引表都放入RAM,这样比放在硬盘更加有效。
  • tweet的内容和基础设施几乎没什么关系。T-bird stores负责tweet所有的东西。大多数tweet内容是放在RAM处理的。如有没再内存中,就用select query将内容抓到内存中。只有在search,Trends,或是What’s Happening pipelines中涉及到内容,hone timeline对此毫不关心。

展望:

  • 如何使通道更快更有效?
  • Fanout可以慢下来。可以调整到5秒以下,但是有时候不能工作。非常难,特别是当有名人tweet时候,这种情况越来越多。
  • Twitter 关注也是非常不对称的。Tweet只提供给定时间内被关注的信息。Twitter更注重你的信息,因为你关注了兰斯.阿姆斯特朗,但是他并没有关注你。由于不存在互相的关注关系,所以社会联系更多是暗示性隐含性。
  • 问题是巨大的基数。@ladygaga 有3100万关注者。@katyperry有2800万。@barackobama有2300万关注着。
  • 当这些人中有人发微博的时候,数据中心需要写大量微薄到各个关注者。当他们开始聊天时候,这是非常大的挑战,而这时刻都在发生着。
  • 这些高关注度的Fanout用户是Twitter最大的挑战。在名人原始微薄发出到关注用户之前,回复一直可以看到。这导致整个网站紊乱。Lady Gaga的一条微薄到关注用户需要几分钟的时间,所以关注者看到这条微薄时间是在不同时间点上。有些人可能需要大概5分钟的时间才能看到这条微薄,这远远落后于其他人。可能早期收到微薄的用户已经收到了回复的列表,而这时候回复还正在处理之中,而fanout还一直进行着所以回复被加了进来。这都发生在延迟关注者收到原始微薄之前。这会导致大量用户混乱。微薄发出之前是通过ID排序的,这导致它们主要是单调增长地,但是那种规模下这不能解决问题。对高值的fanouts队列一直在备份。
  • 试图找到解决合并读和写路径的方法。不在分发高关注度用户微薄。对诸如Taylor Swift的人不会在额外的处理,只需要在读时候,他的时间点并入即可。平衡读和写的路径。可以节约百分之10s的计算资源。

解耦

  • Tweet通过很多的方式解除关联,主要地通过彼此解耦的方式。搜索、推送、邮件兴趣组以及主页时间轴可以彼此独立的工作。
  • F由于性能的原因,系统曾经被解耦过。Twitter曾经是完全同步地方式的,由于性能的原因2年前停止了。提取一个tweet到tweet接口需要花费145微秒,接着所有客户端被断开连接。这是历史遗留的问题。写路径是通过MRI用一个Ruby驱动的程序,一个单线程的服务。每次一个独立的woker被分配的时候,处理器都会被耗尽。他们希望有能力尽可能快的去释放客户端连接。一个tweet进来了。Ruby处理了它。把它插入队列,并且断开连接。他们仅仅运行大概45-48进程/盒。所以他们只能并行处理同样多tweets/盒,所以他们希望尽可能快的断开连接。
  • Tweets 被切换到异步路径方式,我们讨论所有东西都被剔除了。

监控

  • 办公室四处的仪表盘显示了系统在任何给定时间系统的运行状态。
  • 如果你有100万的关注者,将会要花费好几分钟到分发到所有tweets。
  • Tweets 入口统计:每天400兆的tweets。日平均每秒种5000;日高峰时每秒7000;有事件时候是每秒超过12000。
  • 时间轴分发统计:30b分发/日(约21M/分钟);3.5秒@p50(50%分发率)分发到1M;300k 分发/秒;@p99 大概需要5分钟。
  • 一个名为VIZ的监控系统监控各个集群。时间轴服务检索数据集群数据的平均请求时间是5毫秒。@p99需要100毫秒。@p99.9需要请求磁盘,所以需要大概好几百毫秒。
  • Zipkin 是基于谷歌Dapper的系统。利用它可以探测一个请求,查看他点击的每一个服务,以及请求时间,所以可以获得每个请求的每一部分性能细节信息。你还还可以向下钻取,得到不同时间周期下的每单个的请求的详细信息。大部分的时间是在调试系统,查看请求时间都消耗的什么地方。它也可以展示不同纬度的聚合统计,例如,用来查看fanout和分发了多久。大概用了2年长的一个工程,来把活跃用户时间线降到2毫秒。大部分时间用来克服GC停顿,memchache查询,理解数据中心的拓扑应该是怎么样结构,最后建立这种类型集群。
  • 参与翻译(4人):Garfielt, 桔子, 李勇2, yale8848

.NET技术+25台服务器怎样支撑世界第54大网站

摘要:同时使用Linux和Windows平台产品,大量使用静态的方法和类,Stack Overflow是个重度性能控。同时,取代横向扩展,他们坚持着纵向扩展思路,因为“硬件永远比程序员便宜”。


StackOverflow是一个IT技术问答网站,用户可以在网站上提交和回答问题。当下的StackOverflow已拥有400万个用户,4000万个回答,月PV5.6亿,世界排行第54。然而值得关注的是,支撑他们网站的全部服务器只有25台,并且都保持着非常低的资源使用率,这是一场高有效性、负载均衡、缓存、数据库、搜索及高效代码上的较量。近日,High Scalability创始人Todd Hoff根据Marco Cecconi的演讲视频“ The architecture of StackOverflow”以及Nick Craver的博文“ What it takes to run Stack Overflow”总结了StackOverflow的成功原因。


点击查看原图


意料之中,也是意料之外,Stack Overflow仍然重度使用着微软的产品。他们认为既然微软的基础设施可以满足需求,又足够便宜,那么没有什么理由去做根本上的改变。而在需要的地方,他们同样使用了Linux。究其根本,一切都是为了性能。


另一个值得关注的地方是,Stack Overflow仍然使用着纵向扩展策略,没有使用云。他们使用了384GB的内存和2TB的SSD来支撑SQL Servers,如果使用AWS的话,花费可想而知。没有使用云的另一个原因是Stack Overflow认为云会一定程度上的降低性能,同时也会给优化和排查系统问题增加难度。此外,他们的架构也并不需要横向扩展。峰值期间是横向扩展的杀手级应用场景,然而他们有着丰富的系统调整经验去应对。该公司仍然坚持着Jeff Atwood的名言——硬件永远比程序员便宜。


Marco Ceccon曾提到,在谈及系统时,有一件事情必须首先弄明白——需要解决问题的类型。首先,从简单方面着手,StackExchange究竟是用来做什么的——首先是一些主题,然后围绕这些主题建立社区,最后就形成了这个令人敬佩的问答网站。


其次则是规模相关。StackExchange在飞速增长,需要处理大量的数据传输,那么这些都是如何完成的,特别是只使用了25台服务器,下面一起追根揭底:


状态


●StackExchange拥有110个站点,以每个月3到4个的速度增长。

●400万用户

●800万问题

●4000万答案

●世界排名54位

●每年增长100%

●月PV 5.6亿万

●大多数工作日期间峰值为2600到3000请求每秒,作为一个编程相关网站,一般情况下工作日的请求都会高于周末

●25台服务器

●SSD中储存了2TB的SQL数据

●每个web server都配置了2个320G的SSD,使用RAID 1

●每个ElasticSearch主机都配备了300GB的机械硬盘,同时也使用了SSD

●Stack Overflow的读写比是40:60

●DB Server的平均CPU利用率是10%

●11个web server,使用IIS

●2个负载均衡器,1个活跃,使用HAProxy

●4个活跃的数据库节点,使用MS SQL

●3台实现了tag engine的应用程序服务器,所有搜索都通过tag

●3台服务器通过ElasticSearch做搜索

●2台使用了Redis的服务器支撑分布式缓存和消息

●2台Networks(Nexus 5596 + Fabric Extenders)

●2 Cisco 5525-X ASAs

●2 Cisco 3945 Routers

●主要服务Stack Exchange API的2个只读SQL Servers

●VM用于部署、域控制器、监控、运维数据库等场合


平台


●ElasticSearch

●Redis

●HAProxy

●MS SQL

●Opserver

●TeamCity

●Jil——Fast .NET JSON Serializer,建立在Sigil之上

●Dapper——微型的ORM


UI


●UI拥有一个信息收件箱,用于新徽章获得、用户发送信息、重大事件发生时的信息收取,使用WebSockets实现,并通过Redis支撑。

●搜索箱通过 ElasticSearch 实现,使用了一个REST接口。

●因为用户提出问题的频率很高,因此很难显示最新问题,每秒都会有新的问题产生,从而这里需要开发一个关注用户行为模式的算法,只给用户显示感兴趣的问题。它使用了基于Tag的复杂查询,这也是开发独立Tag Engine的原因。

●服务器端模板用于生成页面。


服务器


●25台服务器并没有满载,CPU使用率并不高,单计算SO(Stack Overflow)只需要5台服务器。

●数据库服务器资源利用率在10%左右,除下执行备份时。

●为什么会这么低?因为数据库服务器足足拥有384GB内存,同时web server的CPU利用率也只有10%-15%。

●纵向扩展还没有遇到瓶颈。通常情况下,如此流量使用横向扩展大约需要100到300台服务器。

●简单的系统。基于.Net,只用了9个项目,其他系统可能需要100个。之所以使用这么少系统是为了追求极限的编译速度,这点需要从系统开始时就进行规划,每台服务器的编译时间大约是10秒。

●11万行代码,对比流量来说非常少。

●使用这种极简的方式主要基于几个原因。首先,不需要太多测试,因为Meta.stackoverflow本来就是一个问题和bug讨论社区。其次,Meta.stackoverflow还是一个软件的测试网站,如果用户发现问题的话,往往会提出并给予解决方案。

●纽约数据中心使用的是Windows 2012,已经向2012 R2升级(Oregon已经完成了升级),Linux系统使用的是Centos 6.4。


SSD


●默认使用的是Intel 330(Web层等)

●Intel 520用于中间层写入,比如Elastic Search

●数据层使用Intel 710和S3700

●系统同时使用了RAID 1和RAID 10(任何4+以上的磁盘都使用RAID 10)。不畏惧故障发生,即使生产环境中使用了上千块2.5英寸SSD,还没碰到过一块失败的情景。每个模型都使用了1个以上的备件,多个磁盘发生故障的情景不在考虑之中。

●ElasticSearch在SSD上表现的异常出色,因为SO writes/re-indexes的操作非常频繁。

●SSD改变了搜索的使用方式。因为锁的问题,Luncene.net并不能支撑SO的并发负载,因此他们转向了ElasticSearch。在全SSD环境下,并不需要围绕Binary Reader建立锁。


高可用性


●异地备份——主数据中心位于纽约,备份数据中心在Oregon。

●Redis有两个从节点,SQL有2个备份,Tag Engine有3个节点,elastic有3个节点,冗余一切,并在两个数据中心同时存在。

●Nginx是用于SSL,终止SSL时转换使用HAProxy。

●并不是主从所有,一些临时的数据只会放到缓存中

所有HTTP流量发送只占总流量的77%,还存在Oregon数据中心的备份及一些其他的VPN流量。这些流量主要由SQL和Redis备份产生。


数据库


●MS SQL Server

●Stack Exchange为每个网站都设置了数据库,因此Stack Overflow有一个、Server Fault有一个,以此类推。

●在纽约的主数据中心,每个集群通常都使用1主和1只读备份的配置,同时还会在Oregon数据中心也设置一个备份。如果是运行的是Oregon集群,那么两个在纽约数据中心的备份都会是只读和同步的。

●为其他内容准备的数据库。这里还存在一个“网络范围”的数据库,用于储存登陆凭证和聚合数据(大部分是stackexchange.com用户文件或者API)。

●Careers Stack Overflow、stackexchange.com和Area 51等都拥有自己独立的数据库模式。

●模式的变化需要同时提供给所有站点的数据库,它们需要向下兼容,举个例子,如果需要重命名一个列,那么将非常麻烦,这里需要进行多个操作:增加一个新列,添加作用在两个列上的代码,给新列写数据,改变代码让新列有效,移除旧列。

●并不需要分片,所有事情通过索引来解决,而且数据体积也没那么大。如果有filtered indexes需求,那么为什么不更高效的进行?常见模式只在DeletionDate = Null上做索引,其他则通过为枚举指定类型。每项votes都设置了1个表,比如一张表给post votes,1张表给comment votes。大部分的页面都可以实时渲染,只为匿名用户缓存,因此,不存在缓存更新,只有重查询。

●Scores是非规范化的,因此需要经常查询。它只包含IDs和dates,post votes表格当下大约有56454478行,使用索引,大部分的查询都可以在数毫秒内完成。

●Tag Engine是完全独立的,这就意味着核心功能并不依赖任何外部应用程序。它是一个巨大的内存结构数组结构,专为SO用例优化,并为重负载组合进行预计算。Tag Engine是个简单的windows服务,冗余的运行在多个主机上。CPU使用率基本上保持在2-5%,3个主机专门用于冗余,不负责任何负载。如果所有主机同时发生故障,网络服务器将把Tag Engine加载到内存中持续运行。

●关于Dapper无编译器校验查询与传统ORM的对比。使用编译器有很多好处,但在运行时仍然会存在fundamental disconnect问题。同时更重要的是,由于生成nasty SQL,通常情况还需要去寻找原始代码,而Query Hint和parameterization控制等能力的缺乏更让查询优化变得复杂。


编码


●流程

●大部分程序员都是远程工作,自己选择编码地点

●编译非常快

●然后运行少量的测试

●一旦编译成功,代码即转移至开发交付准备服务器

●通过功能开关隐藏新功能

●在相同硬件上作为其他站点测试运行

●然后转移至Meta.stackoverflow测试,每天有上千个程序员在使用,一个很好的测试环境

●如果通过则上线,在更广大的社区进行测试

●大量使用静态类和方法,为了更简单及更好的性能

●编码过程非常简单,因为复杂的部分被打包到库里,这些库被开源和维护。.Net 项目数量很低,因为使用了社区共享的部分代码。

●开发者同时使用2到3个显示器,多个屏幕可以显著提高生产效率。


缓存



●缓存一切

●5个等级的缓存

●1级是网络级缓存,缓存在浏览器、CDN以及代理服务器中。

●2级由.Net框架 HttpRuntime.Cache完成,在每台服务器的内存中。

●3级Redis,分布式内存键值存储,在多个支撑同一个站点的服务器上共享缓存项。

●4级SQL Server Cache,整个数据库,所有数据都被放到内存中。

●5级SSD。通常只在SQL Server预热后才生效。

●举个例子,每个帮助页面都进行了缓存,访问一个页面的代码非常简单:

●使用了静态的方法和类。从OOP角度来看确实很糟,但是非常快并有利于简洁编码。

●缓存由Redis和Dapper支撑,一个微型ORM

●为了解决垃圾收集问题,模板中1个类只使用1个副本,被建立和保存在缓存中。监测一切,包括GC操。据统计显示,间接层增加GC压力达到了某个程度时会显著的降低性能。

●CDN Hit 。鉴于查询字符串基于文件内容进行哈希,只在有新建立时才会被再次取出。每天3000万到5000万Hit,带宽大约为300GB到600GB。

●CDN不是用来应对CPU或I/O负载,而是帮助用户更快的获得答案


部署


●每天5次部署,不去建立过大的应用。主要因为

●可以直接的监视性能

●尽可能最小化建立,可以工作才是重点

●产品建立后再通过强大的脚本拷贝到各个网页层,每个服务器的步骤是:

●通过POST通知HAProxy下架某台服务器

●延迟IIS结束现有请求(大约5秒)

●停止网站(通过同一个PSSession结束所有下游)

●Robocopy文件

●开启网站

●通过另一个POST做HAProxy Re-enable

●几乎所有部署都是通过puppet或DSC,升级通常只是大幅度调整RAID阵列并通过PXE boot安装,这样做非常快速。


协作

●团队

●SRE (System Reliability Engineering):5人

●Core Dev(Q&A site)6-7人

●Core Dev Mobile:6人

●Careers团队专门负责SO Careers产品开发:7人

●Devops和开发者结合的非常紧密

●团队间变化很大

●大部分员工远程工作

●办公室主要用于销售,Denver和London除外

●一切平等,些许偏向纽约工作者,因为面对面有助于工作交流,但是在线工作影响也并不大

●对比可以在同一个办公室办公,他们更偏向热爱产品及有才华的工程师,他们可以很好的衡量利弊

●许多人因为家庭而选择远程工作,纽约是不错,但是生活并不宽松

●办公室设立在曼哈顿,那是个人才的诞生地。数据中心不能太偏,因为经常会涉及升级

●打造一个强大团队,偏爱极客。早期的微软就聚集了大量极客,因此他们征服了整个世界

●Stack Overflow社区也是个招聘的地点,他们在那寻找热爱编码、乐于助人及热爱交流的人才。


编制预算



●预算是项目的基础。钱只花在为新项目建立基础设施上,如此低利用率的 web server还是3年前数据中心建立时购入。


测试


●快速迭代和遗弃

●许多测试都是发布队伍完成的。开发拥有一个同样的SQL服务器,并且运行在相同的Web层,因此性能测试并不会糟糕。

●非常少的测试。Stack Overflow并没有进行太多的单元测试,因为他们使用了大量的静态代码,还有一个非常活跃的社区。

●基础设施改变。鉴于所有东西都有双份,所以每个旧配置都有备份,并使用了一个快速故障恢复机制。比如,keepalived可以在负载均衡器中快速回退。

●对比定期维护,他们更愿意依赖冗余系统。SQL备份用一个专门的服务器进行测试,只为了可以重存储。计划做每两个月一次的全数据中心故障恢复,或者使用完全只读的第二数据中心。

●每次新功能发布都做单元测试、集成测试盒UI测试,这就意味着可以预知输入的产品功能测试后就会推送到孵化网站,即meta.stackexchange(原meta.stackoverflow)。


监视/日志


●当下正在考虑使用http://logstash.net/做日志管理,目前使用了一个专门的服务将syslog UDP传输到SQL数据库中。网页中为计时添加header,这样就可以通过HAProxy来捕获并且融合到syslog传输中。

●Opserver和Realog用于显示测量结果。Realog是一个日志展示系统,由Kyle Brandt和Matt Jibson使用Go建立。

●日志通过HAProxy负载均衡器借助syslog完成,而不是IIS,因为其功能比IIS更丰富。


关于云


●还是老生常谈,硬件永远比开发者和有效率的代码便宜。基于木桶效应,速度肯定受限于某个短板,现有的云服务基本上都存在容量和性能限制。

●如果从开始就使用云来建设SO说不定也会达到现在的水准。但毫无疑问的是,如果达到同样的性能,使用云的成本将远远高于自建数据中心。


性能至上


●StackOverflow是个重度的性能控,主页加载的时间永远控制在50毫秒内,当下的响应时间是28毫秒。

●程序员热衷于降低页面加载时间以及提高用户体验。

●每个独立的网络提交都予以计时和记录,这种计量可以弄清楚提升性能需要修改的地方。

●如此低资源利用率的主要原因就是高效的代码。web server的CPU平均利用率在5%到15%之间,内存使用为15.5 GB,网络传输在20 Mb/s到40 Mb/s。SQL服务器的CPU使用率在5%到10%之间,内存使用是365GB,网络传输为100 Mb/s到200 Mb/s。这可以带来3个好处:给升级留下很大的空间;在严重错误发生时可以保持服务可用;在需要时可以快速回档。


学到的知识



1.为什么使用MS产品的同时还使用Redis?什么好用用什么,不要做无必要的系统之争,比如C#在Windows机器上运行最好,我们使用IIS;Redis在*nix机器上可以得到充分发挥,我们使用*nix。


2. Overkill即策略。平常的利用率并不能代表什么,当某些特定的事情发生时,比如备份、重建等完全可以将资源使用拉满。


3. 坚固的SSD。所有数据库都建立在SSD之上,这样可以获得0延时。

4. 了解你的读写负载。


5. 高效的代码意味着更少的主机。只有新项目上线时才会因为特殊需求增加硬件,通常情况下是添加内存,但在此之外,高效的代码就意味着0硬件添加。所以经常只讨论两个问题:为存储增加新的SSD;为新项目增加硬件。


6. 不要害怕定制化。SO在Tag上使用复杂查询,因此专门开发了所需的Tag Engine。


7. 只做必须做的事情。之所以不需要测试是因为有一个活跃的社区支撑,比如,开发者不用担心出现“Square Wheel”效应,如果开发者可以制作一个更更轻量级的组件,那就替代吧。


8. 注重硬件知识,比如IL。一些代码使用IL而不是C#。聚焦SQL查询计划。使用web server的内存转储究竟做了些什么。探索,比如为什么一个split会产生2GB的垃圾。


9. 切勿官僚作风。总有一些新的工具是你需要的,比如,一个编辑器,新版本的Visual Studio,降低提升过程中的一切阻力。


10. 垃圾回收驱动编程。SO在减少垃圾回收成本上做了很多努力,跳过类似TDD的实践,避免抽象层,使用静态方法。虽然极端,但是确实打造出非常高效的代码。


11. 高效代码的价值远远超出你想象,它可以让硬件跑的更快,降低资源使用,切记让代码更容易被程序员理解。


来源HighScalability,CSDN编译整理(编译/仲浩 审校/魏伟)

链接:http://www.csdn.net/article/2014-07-22/2820774-stackoverflow-update-560m-pageviews-a-month-25-servers

微信、陌陌等著名IM软件设计架构详解

对微信、陌陌等进行了分析,发出来分享一下(时间有些久了)
电量:对于移动设备最大的瓶颈就是电量了。因为用户不可能随时携带电源,充电宝。所以必须考虑到电量问题。那就要检查我们工程是不是有后台运行,心跳包发送时间是不是合理。
流量:对于好多国内大部分屌丝用户来说可能还是包月30M,那么我们必须站在广大用户角度来考虑问题了。一个包可以解决的就一个包。
点击查看原图
网络:
这个也是IM最核心的内容了,我们要做到在任何网络下等顺畅聊天那就不容易了,好多公司都用的xmpp框架,如果在强网络环境下,xmpp完全没有问题。但是那种弱网络环境下xmpp就束手无策啦,用户体验就很垃圾了。
个人觉得xmpp 可以玩玩(参考看这个 RFC3920RFC3921   ), 但是用来真正的产品就差远了。如果遇到一个做IM 的朋友张口闭口都说xmpp 的话,那么不用沟通了,肯定不是什么好产品。微信、QQ以前也曾用过xmpp,但是最后也放弃了xmpp,就知道xmpp有很多弊端了,还有就是报文太大,好臃肿,浪费流量。为了保证稳定,微信用了长链接和短链接相结合,例如:
1 、两个域名
微信划分了http模式(short链接)和 tcp 模式(long 链接),分别应对状态协议和数据传输协议
long.weixin.qq.com  dns check 112.64.237.188 112.64.200.218
 short.weixin.qq.com  dns check  ( 112.64.237.186 112.64.200.240)
说明
 HTTP协议扩展,运行8080 端口http body为二进制(protobuf)。
 主要用途(接口)
用户登录验证;
好友关系(获取,添加);
消息sync (newsync),自有sync机制;
获取用户图像;
用户注销;
行为日志上报。
朋友圈发表刷新
 2.2  long.weixin.qq.com  
  tcp 长连接, 端口为8080,类似微软activesync的二进制协议。
 主要用途(接口)
接受/发送文本消息;
接受/发送语音;
接受/发送图片;
接受/发送视频文件等。
 
 所有上面请求都是基于tcp长连接。在发送图片和视频文件等时,分为两个请求;第一个请求是缩略图的方式,第二个请求是全数据的方式。
 
2.2.1 数据报文方面
增量上传策略:
每次8k左右大小数据上传,服务器确认;在继续传输。
 
图片上传:
先传缩略图,传文本消息,再传具体文件
 
下载:
先下载缩略图, 在下载原图
下载的时候,全部一次推送。
附录
3.1  用户登录验证
POST /cgi-bin/micromsg-bin/auth HTTP/1.1
Accept: **
User-Agent: Mozilla/4.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 174
 
3.3 消息sync (newsync)
POST /cgi-bin/micromsg-bin/newsync HTTP/1.1
User-Agent: Android QQMail HTTP Client
Cache-Control: no-cache
Connection: Keep-Alive
Content-Type: application/octet-stream
accept: **
Content-Length:  206
 
3.5 用户注销
POST /cgi-bin/micromsg-bin/iphoneunreg HTTP/1.1
Accept: */*
User-Agent: Mozilla/4.0
Cotent-Type: application/x-www-form-urlencoded
Content-Length: 271
 
3.6 行为日志上报
POST /cgi-bin/stackreport?version=240000a7&filelength=1258&sum=7eda777ee26a76a5c93b233eed504c7d&reporttype=1&username=jolestar HTTP/1.1
Content-Length: 736
Content-Type: binary/octet-stream
Connection: Keep-Alive
User-Agent: Apache-HttpClient/UNAVAILABLE (java 1.4)
从现在互联网的发展而言,IM和视频(包括IM里面视频通话)是一个方向,这些都应该成为互联网的基础设施,就像浏览器一样。现在IM还没有一个很好的解决方案,XMPP并不能很好地做到业务逻辑独立开来。从IM的本质来看,IM其实就是将一条消息从一个地方传输到另外一个地方,这个和TCP很像,为什么不实现一个高级点的TCP协议了,只是将TCP/IP里面的IP地址换成了一个类似XMPP的唯一ID而已,其他的很多细节都可以照搬TCP协议。有了这个协议之后,将业务逻辑在现有HTTP server的基础上做,例如发送语音和图片就相当于上传一个文件,服务器在处理完这个文件后就发一条特殊的IM消息。客户端收到这个IM消息后,按照IM消息里面urlHTTP server取语音文件和图片文件。将HTTP serverIM server打通之后,可以做很多事情。但将这个两个server合并在一块并不是一个好事,不然腾讯也不会有2005年的战略转型了。从现在的情况来看,应用除了游戏,都没怎么赚钱,现在能够承载赚钱业务的还是以web为主。IM不可以赚钱,但没有却是不行的,就像一个地方要致富,不修路是不行的道理一样。

上面说到了protobuf ,就简单介绍下:

          JSON相信大家都知道是什么东西,如果不知道,那可就真的OUT了,GOOGLE一下去。这里就不介绍啥的了。

Protobuffer大家估计就很少听说了,但如果说到是GOOGLE搞的,相信大家都会有兴趣去试一下,毕竟GOOGLE出口,多属精品。

Protobuffer是一个类似JSON的一个传输协议,其实也不能说是协议,只是一个数据传输的东西罢了。

那它跟JSON有什么区别呢?

        跨语言,这是它的一个优点。它自带了一个编译器,protoc,只需要用它进行编译,可以编译成JAVApythonC++代码,暂时只有这三个,其他就暂时不要想了,然后就可以直接使用,不需要再写任何其他代码。连解析的那些都已经自带有的。JSON当然也是跨语言的,但这个跨语言是建立在编写代码的基础上。



陌陌设计:
陌陌发展刚开始由于规模小,30-40W连接数(包括Android后台长连接用户),也使用XMPP由于XMPP的缺点:流量大(基于XML),不可靠(为传统固定网络设计,没有考虑WIFI/2G/3G/地铁/电梯等复杂网络场景),交互复杂(登陆需5-6次,尤其是TLS握手);XMPP丢消息的根本原因:服务端和客户端处于半关闭状态,客户端假连接状态,服务端有收不到回执;Server端连接层和逻辑层代码没有解耦分离,常常重启导致不可用;
点击查看原图

消息中转:
点击查看原图
链接层:
点击查看原图
逻辑层:
点击查看原图
通讯协议设计:
点击查看原图
高效:弱网络快速的收发
可靠:不会丢消息
易于扩展
协议格式:
点击查看原图
Redis协议:

点击查看原图


点击查看原图点击查看原图




点击查看原图
是啊发
点击查看原图
点击查看原图
点击查看原图
阿萨德发阿发a
点击查看原图
优化
   连接层(参见通讯服务器组成):只做消息转发,允许随时重启更新,设计原则简单/异步;单台压测试连接数70W;现状:1.5亿用户,月活5000W+,连接数1200W+
   逻辑层(参见通讯服务器组成):用户会话验证即登陆、消息存取、异步队列
   采用私有通讯协议,目标:高效,弱网络快速收发;可靠:不会丢消息;易于扩展;参考协议格式:REDIS协议;参见协议格式、基于队列的消息协议、基于队列的交互、基于版本号的消息协议、基于版本号的交互等;
   核心的长连接只用于传输轻量的实时数据,图片、语音等都开新的TCPHTTP连接;
            一切就绪后,最重要的就是监控,写一个APP查看所有的运营状态,每天观察;
   如何选择最优路线,即智能路由;
二、智能路由、连接策略:
多端口、双协议支持
应对移动网关代理的端口限制
         支持TCPHTTP两种协议
         根据备选IP列表进行并发测速(IP+端口+协议)
         后端根据终端连接情况,定时更新终端的备选IP列表
         终端在连接空闲时上报测速数据,便于后端决策
         TCP协议不通,自动切换到http
         优先使用最近可用IP
        并发测速,根据终端所处的位置下发多组IPPORT,只用IP,不用域名,手机上的DNS50%不准

负载均衡器(LVS...)的问题– 单点失效

      单点性能瓶颈
      负载均衡从客户端开始做起• 域名负载的问题

      域名系统不可靠– 更新延迟大  

WNS(wireless network services)
点击查看原图
1解决移动互联网开发常见问题:
通道:数据交互、大数据上传、push
网络连接:大量长链接管理、链接不上、慢、多地分布
运营支撑:海量监控、简化问题定位
登录&安全:登录鉴权、频率控制

点击查看原图点击查看原图

移动互联网特点:
1、高延时:  信道建立耗时( 高RTT)
2、低宽带、高丢包
3、多运营商(电信,移动,联通等)
4、复杂网络  
    -2G,3G,4G,wifi。cmwap,cmnet。。
    -网关限制:协议,端口
5、用户流动性大,上网环境复杂

WNS 性能指标:
点击查看原图点击查看原图点击查看原图点击查看原图
1、开发时间:历史一年半
2、链接成功率-99.9%
3、极端网络环境下成功率-由于常见app
4、crash率 -0.02%(crash次数/登录用户数)

微信后台系统架构
背景:
A、分布式问题收敛
  后台逻辑模块专注逻辑,快速开发
可能读取到过时的数据是个痛点
需要看到一致的数据
B、内部定义
  数据拥有两个以上的副本
如果成功提交了变更,那么不会再返回旧数据
推演:
1增加一个数据
2 序列号发生器,偏序
 约束:只能有一个client操作
client有解决冲突的能力
问题转移:client如何分布?
3 修改集群中一个制定的key的value
1)覆盖他
2)根据value的内容做修改
if value = 1 then value :=2 
通用解法:
1)paxos算法
 工程难度
一切可控

分布式算法设计:
 2)quorum算法(2011)
再单个key上面运算
真是系统约束
类paxos方案,简化
为每次变更选举(by key)
算法过程
 提议/变更/同步/广播

系统架构
点击查看原图
写流程
点击查看原图
Replication & Sharding 
 权衡点
  自治,负载均衡,扩散控制
 replication->relation

容灾抵消
同城(上海)多数派存活
三园区(独立供电,独立)


Sharding

一组KV6 为一个单位

1、人工阶段
 局部扩容,影响收敛9
2均匀分布 制定分段hash32 (string)
 翻倍扩容

3一致性哈希
具体实现?
1、业务侧快速开发
 存储需要提供强一致性
丰富的数据模型支持(结构化、类SQL/KV)
条件读,条件写

2 业务增长迅速,系统要能够方便的横向扩容
3设备故障/短时节点实效便成为常态,容灾自动化,主碑可写无需人工介入
4小数据
点击查看原图
存储模型
 纯内存
Bitcask
小表系统
LSM-tree
点击查看原图
小表系统
1、解决放大问题
2、数据按变更聚集存储
3、Affected1
   ChangeTable
(1+2+。。。。+n-1+total)/n
4、分裂与合并
数据流动
1、自动化迁移
2、节点同时做代理
3、合并磁盘io

同步流量
1、数据vs 操作
2、幂等
3、保底策略

通信包量
 1、动态合并
    100K qps
    200% -10%
3、权衡与估算
设计要点
1、吞吐量
2、异步化
3、复杂度
4、libco
自动修复系统
1、不要让错误累计
2、全量扫描
bitcask 的一些变化
1、内存限制
2、全内存

IF-ERRORLEVEL

@echo off
: p
ping 61.152.XX.XX
IF ERRORLEVEL 1 goto aa
IF ERRORLEVEL 0 goto bb
:aa
echo 正在重启软件服务...
net stop MSSQLSERVER
net start MSSQLSERVER
goto p
:bb
echo 继续ping中。。。
goto p

 

 

if ERRORLEVEL nubmer commend
例:if ERRORLEVEL 1 echo 返回值等于1

那么大家就要问了,我怎么获取到前一个命令的返回值呢?
方法很简单,拿ping来说,我们先运行一个ping 127.0.0.1
运行结果是Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
这时候我们输入,echo %errorlevel%,就可以看到返回值是0,
然后我们在ping一个不存在的ip,比如192.192.192.192
ping命令显示 Request timed out.。
ok,我们在用echo %errorlevel%查一下返回值是多少。
现在变成了1。

你也可以用这个方法来查询其他命令的返回值,比如copy、dir等等。
当然if ERRORLEVEL对数值的比较方法不仅仅限于等于。在微软的帮助中我们可以看到:

IF %ERRORLEVEL% LEQ 1 goto okay

这里的LEQ表示“小于等于”,全部的比较参数如下:
EQU - 等于
NEQ - 不等于
LSS - 小于
LEQ - 小于或等于
GTR - 大于
GEQ - 大于或等于

如有错误,感谢指正。

IF-ERRORLEVEL

建立TEST4.BAT,内容如下:
@ECHO OFF 
XCOPY C:\AUTOEXEC.BAT D:\
IF ERRORLEVEL 1 ECHO 文件拷贝失败 
IF ERRORLEVEL 0 ECHO 成功拷贝文件 
然后执行文件:
C:\>TEST4
如果文件拷贝成功,屏幕就会显示“成功拷贝文件”,否则就会显示“文件拷贝失败”。 
IF ERRORLEVEL 是用来测试它的上一个DOS命令的返回值的,注意只是上一个命令的返回值,而且返回值必须依照从大到小次序顺序判断。
因此下面的批处理文件是错误的:
@ECHO OFF 
XCOPY C:\AUTOEXEC.BAT D:\ 
IF ERRORLEVEL 0 ECHO 成功拷贝文件 
IF ERRORLEVEL 1 ECHO 未找到拷贝文件 
IF ERRORLEVEL 2 ECHO 用户通过ctrl-c中止拷贝操作 
IF ERRORLEVEL 3 ECHO 预置错误阻止文件拷贝操作 
IF ERRORLEVEL 4 ECHO 拷贝过程中写盘错误 
无论拷贝是否成功,后面的: 
未找到拷贝文件 
用户通过ctrl-c中止拷贝操作 
预置错误阻止文件拷贝操作 
拷贝过程中写盘错误
都将显示出来。 
以下就是几个常用命令的返回值及其代表的意义: 
backup 
0 备份成功 
1 未找到备份文件 
2 文件共享冲突阻止备份完成 
3 用户用ctrl-c中止备份 
4 由于致命的错误使备份操作中止 
diskcomp 
0 盘比较相同 
1 盘比较不同 
2 用户通过ctrl-c中止比较操作 
3 由于致命的错误使比较操作中止 
4 预置错误中止比较 
diskcopy 
0 盘拷贝操作成功 
1 非致命盘读/写错 
2 用户通过ctrl-c结束拷贝操作 
3 因致命的处理错误使盘拷贝中止 
4 预置错误阻止拷贝操作 
format 
0 格式化成功 
3 用户通过ctrl-c中止格式化处理 
4 因致命的处理错误使格式化中止 
5 在提示“proceed with format(y/n)?”下用户键入n结束 
xcopy 
0 成功拷贝文件 
1 未找到拷贝文件 
2 用户通过ctrl-c中止拷贝操作 
4 预置错误阻止文件拷贝操作 
5 拷贝过程中写盘错误

Imagemagick resize

一些其它参考:

http://blog.sina.com.cn/s/blog_ba532aea0101bty5.html

 

将一个200x304大小的图像缩小为100x152(长宽各缩短一半)。
convert page200.png -resize 100x152 page100.png
上面的 100x152,指定了目标文件的长和宽。
 你也可以只指定目标文件宽度,这样它的高度会等比例放大:
convert page200.png -resize 100 page100.png
或者只指定高度:
convert page200.png -resize x152 page100.png
以上三句命令,其结果都是一样的。

 

一些命令:

 

1,获取图片信息

$identify image.png 
image.png PNG 559x559 559x559+0+0 8-bit sRGB 467KB 0.000u 0:00.008 
如果只需要获取宽高:

$identify -format "%wx%h" image.png 
2,放大,缩小 -resize

$convert image.png -resize 200x200 resize.png 
也可以按照比例(缩小一半):

$convert image.png -resize 50% resize.png 
还可以多次缩放(先缩小一半,再放大一倍,效果就是变模糊了):

$convert image.png -resize 50%  -resize 200%  resize.png 
3,放大,缩小 -sample

与resize的区别在于-sample只进行了采样,没有进行插值,所以用来生成缩略图最合适

$convert image.png -sample 50% sample.png 
这个处理的效果就是马赛克:

$convert image.png -sample 10% -sample 1000% sample.png 
4,裁剪 -crop

从(50,50)位置开始,裁剪一个100X100大小的图片:

$convert image.png -crop 100x100+50+50 crop.png 
如果不指定位置,则按照这个大小分隔出小图片,这个命令生成crop-0.png,crop-1.png,crop-2.png……:

$convert image.png -crop 100x100 crop.png 
可以指定裁剪位置的相对位置 -gravity:

$convert image.png -gravity northeast -crop 100x100+0+0 crop.png 
-gravity即指定坐标原点,有northwest:左上角,north:上边中间,northeast:右上角,east:右边中间……
5,旋转 -rotate

$convert image.png -rotate 45 rotate.png 
默认的背景为白色,我们可以指定背景色:

$convert image.png -backround black -rotate 45 rotate.png 
$convert image.png -background #000000 -rotate 45 rotate.png 
还可以指定为透明背景色:

$convert image.png -background rgba(0,0,0,0) -rotate 45 rotate.png 
6,合并

合并指的是将一张图片覆盖到一个背景图片上:

$convert image.png -compose over overlay.png -composite newimage.png 
-compose指定覆盖操作的类型,其中over为安全覆盖,另外还有xor、in、out、atop等等
覆盖的位置可以通过-gravity指定:

$convert image.png -gravity southeast -compose over overlay.png -composite newimage.png 
这是将图片覆盖到底图的右下角。
7,更改图片的alpha通道

分两步:

$convert image.png -define png:format=png32  image32.png 
$convert image32.png -channel alpha -fx "0.5" imagealpha.png 
这个命令首先将image.png的格式改为png32(确保有alpha通道),然后更改alpha通道置为0.5,也就是半透明,值的范围为0到1.0
可以使用将一张透明图片覆盖到原图上的方式做水印图片:

$convert image.png -gravity center -compose over overlay.png -composite newimage.png 
$convert image.png -gravity southeast -compose over overlay.png -composite newimage.png 
8,拼接

横向拼接(+append),下对齐(-gravity south):

$convert image1.png image2.png image3.png -gravity south +append result.png 
纵向拼接(-append),右对齐(-gravity east):

$convert image1.png image2.png image3.png -gravity east -append result.png 
9,格式转换

$convert image.png image.jpg 
$convert image.png -define png:format=png32 newimage.png 
10,文字注释

$convert image.png -draw "text 0,20 'some text'" newimage.png 
从文件text.txt中读取文字,指定颜色,字体,大小,位置:

$convert source.jpg -font xxx.ttf -fill red -pointsize 48 -annotate +50+50 @text.txt result.jpg 
11,去掉边框
$convert image.png -trim -fuzz 10% newimage.png

12,GIF 需要两步
用imagemagick生成gif的缩略图不能一步到位,需要两步操作

第一步 : convert sinykk.gif -coalesce sinykk_coalesce.gif

第二步 : convert  sinykk_coalesce.gif -resize "300X300>"  sinykk_end.gif

其中: "300X300>" 这个是当GIF图片没有300的长宽将不进行长宽操作

pptp

1、检查服务器是否有必要的支持。如果检查结果没有这些支持的话,是不能安装pptp的。执行指令:
#modprobe ppp-compress-18 && echo ok(用模块方式支持MPPE加密模式浏览,如果内核支持检测不到。)
这条执行执行后,显示“ok”则表明通过。

2、安装ppp和iptables。默认情况下,完整的CentOS是带有这两个组件的,但是精简版的系统可能没有。我们输入下面的命令来确认,如果没有则进行安装,有的话系统不会做任何动作:
#yum install -y iptables

3、安装pptp。这个软件在yum源里是没有的,我们需要手动下载。
加入yum源:
#rpm -Uvh http://poptop.sourceforge.net/yum/stable/rhel6/pptp-release-current.noarch.rpm
#yum install  -y pptpd

4、配置pptp。首先我们要编辑/etc/pptpd.conf文件:
#vim /etc/pptpd.conf

localip 192.168.11.1
remoteip 192.168.11.2-102  (最大限制100个连接)
将前面的“#”注释符去掉,更改为你期望的IP段值。localip表示服务器的IP,remoteip表示分配给客户端的IP地址,可以设置为区间。这里我们使用pptp默认的配置:

再编辑/etc/ppp/options.pptpd文件,为VPN添加Google DNS:
#vim /etc/ppp/options.pptpd
在末尾添加下面两行:
ms-dns 8.8.8.8
ms-dns 8.8.4.4

注意:最好打开这个文件里的debug选项(去掉debug前面的“#”),以方便我们在出错的查看日志排错。错误日志在/var/log/messages里,用命令:cat /var/log/messages | grep pptpd 查看有关PPTP的错误信息。

5、设置pptp VPN账号密码。我们需要编辑/etc/ppp/chap-secrets这个文件:
#vim /etc/ppp/chap-secrets
在这个文件里面,按照“用户名 pptpd 密码 *”的形式编写,一行一个账号和密码。比如添加用户名为test,密码为1234的用户,则编辑如下内容:
test pptpd 1234 *

6、修改内核设置,使其支持转发。编辑/etc/sysctl.conf文件:
#vim /etc/sysctl.conf
将“net.ipv4.ip_forward”改为1,变成下面的形式:
net.ipv4.ip_forward=1
保存退出,并执行下面的命令来生效它:
#sysctl -p

7、添加iptables转发规则。经过前面的6个步骤,我们的VPN已经可以拨号了,但是还不能访问任何网页。最后一步就是添加iptables转发规则了,输入下面的指令:
两种修改方法:命令行或修改/etc/sysconf/iptables 文件
iptables -t nat -A POSTROUTING -o eth0 -s 192.168.11.0/24 -j MASQUERADE

iptables -L -t nat 可以看到:
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  192.168.11.0/24      anywhere
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

在/etc/sysconf/iptables 文件 允许ping
-A RH-Firewall-1-INPUT -s 192.168.11.0/255.255.255.0 -p icmp -m icmp --icmp-type 8 -j ACCEPT
service iptables save  保存
service iptables restart 重启

8.
设置开机自动运行服务。我们最后一步是将pptp和iptables设置为开机自动运行,这样就不需要每次重启服务器后手动启动服务了。当然你不需要自动启动服务的话可以忽略这一步。输入指令:
#chkconfig pptpd on
#chkconfig iptables on

 

限制pptp vpn用户单个连接的简单方法

在/etc/ppp文件夹下面建立一个名为auth-up的文件。在里面写入如下内容即可:

#!/bin/sh
# get the username/ppp line number from the parameters
REALDEVICE=$1
USER=$2
# create the directory to keep pid files per user
mkdir -p /var/run/pptpd-users
# if there is a session already for this user, terminate the old one
if [ -f /var/run/pptpd-users/$USER ]; then
kill -HUP `cat /var/run/pptpd-users/$USER`
fi
# copy the pid file of current user to /var/run/pptpd-users
cp "/var/run/$REALDEVICE.pid" /var/run/pptpd-users/$USER

来源:http://vastars.info/linux/pptp-vpn.html

 

VPN 架设中一些问题的解决方法(pptpd)

1. windows 10 下服务器推送DNS无效

控制面板 > 网络与共享 > VPN所属适配器 > 属性 > IPv4 > 高级 > 取消自动跃点并设置值为 15

IPv6 同上

Control Panel > Network and Sharing Center > Change adapter settings > Right click your Ethernet or Wifi adapter > Properties > double click IPv4 > Advanced > Uncheck Automatic Metric > Enter 15 for interface metric > OK > OK.

On that same Properties page, double click IPv6 > Advanced > Uncheck Automatic Metric > Enter 15 for interface metric > OK > OK.

2.VPN可以连接成功,但不能正常上网,messages中记录如下:

       Cannot determine ethernet address for proxy ARP

   该问题主要出在没有相关的转发规则。需要进行如下配置:

   a.打开配置文件/etc/sysctl.conf,修改配置项net.ipv4.ip_forward为1:    

# Controls IP packet forwarding

net.ipv4.ip_forward = 1

       该配置项用于允许ip转发。

   b.还需在iptables中加入NAT转换:

       iptables -t nat -A POSTROUTING -s 192.168.0.0/255.255.255.0 -j SNAT --to-source 192.168.0.88

       其中192.168.0.0/255.255.255.0为VPN的内部网络,192.168.0.88当然就是服务器的地址了。

3. 如sysctl -p

error: "net.bridge.bridge-nf-call-ip6tables" is an unknown key
error: "net.bridge.bridge-nf-call-iptables" is an unknown key

error: "net.bridge.bridge-nf-call-arptables" is an unknown key

调用并重试pptpd

modprobe bridge
lsmod|grep bridge

 

Windows7 619一些参考:

http://www.wanghailin.cn/linux-pptpd-619-internet/

GRE: read(fd=6,buffer=b77c8480,len=8196) from PTY failed:

Changing /etc/ppp/pptpd-options to the following options worked for me:

refuse-pap
refuse-chap
refuse-mschap
#require-chap
#require-mschap
#require-mschap-v2
#require-mppe-128
mppe-stateful

 

 

CentOS 7 参考:

http://www.wanghailin.cn/centos-7-vpn/

 

一些MTU参考:

Ethernet MinSize = 512bit = 64 Byte
Ethernet MaxSize = 1518 Byte
so Ethernet IP MTU = 1518 – 18 ( 6 SRCMAC+ 6 DSTMAC+ 2 TYPE+ 4 CRC) = 1500 B
so Ethernet IP TCP MSS = 1500 – 40 ( 20 IP_HEADER + 20 TCP_HEADER) = 1460 B
so Ethernet IP UDP MTU/MRU = 1500 – 28 ( 20 IP_HEADER + 8 UDP_HEADER ) = 1472 B
so PPPoE MTU/MRU = 1500 – 8 ( 6 PPPoE_SESSION + 2 PPP_HEADER ) = 1492 B
so TCP over PPPoE MSS = 1492 ( PPPoE MTU/MRU ) – 40 ( 20 IP_HEADER + 20 TCP_HEADER) = 1452
so PPTP MTU/MRU = 1500 – 56 ( 20 IP_HEADER + 20 TCP_HEADER + 12 GRE_HEADER + 4 PPP_HEADER ) = 1444 B
so TCP over PPTP MSS = 1444 ( PPTP MTU/MRU ) – 40 ( 20 IP_HEADER + 20 TCP_HEADER) = 1404
so L2TP MTU/MRU = 1500 – 40 ( 20 IP_HEADER +8 UDP_HEADER + 8 L2TP_HEADER + 4 PPP_HEADER ) = 1460 B
so TCP over L2TP MSS = 1460 ( L2TP MTU/MRU ) – 40 ( 20 IP_HEADER + 20 TCP_HEADER) = 1420 B

so
PPTP over PPPoE MTU/MRU = 1492 ( PPPoE MTU/MRU ) – 56 ( 20 IP_HEADER +
20 TCP_HEADER + 12 GRE_HEADER + 4 PPP_HEADER ) = 1436 B
so PPTP over PPTP MTU/MRU = 1444 ( PPTP MTU/MRU ) – 56 ( 20 IP_HEADER + 20 TCP_HEADER + 12 GRE_HEADER + 4 PPP_HEADER ) = 1388 B
so PPTP over L2TP MTU/MRU = 1460 ( L2TP MTU/MRU ) – 56 ( 20 IP_HEADER + 20 TCP_HEADER + 12 GRE_HEADER + 4 PPP_HEADER ) = 1404 B
so L2TP over PPPoE MTU/MRU = 1492 ( PPPoE MTU/MRU ) – 40 ( 20 IP_HEADER +8 UDP_HEADER + 8 L2TP_HEADER + 4 PPP_HEADER ) = 1452 B
so L2TP over PPTP MTU/MRU = 1444 ( PPTP MTU/MRU ) – 40 ( 20 IP_HEADER +8 UDP_HEADER + 8 L2TP_HEADER + 4 PPP_HEADER ) = 1404 B
so L2TP over L2TP MTU/MRU = 1460 ( L2TP MTU/MRU ) – 40 ( 20 IP_HEADER +8 UDP_HEADER + 8 L2TP_HEADER + 4 PPP_HEADER ) = 1420 B