Author Archives: lazypeople

About lazypeople

coder@SINA

最低成本搭建一个Rocket.chat

搭建私有聊天群组的必要性

大厂的封杀

当前,企业级别或者咨询类的自媒体个人除了考虑使用发光发热的微博、微信等大厂提供的社交服务的工具外,应该还是要考虑搭建一个属于自己的、免受厂商政策影响的私密聊天工具。说到这里有一个极端的例子,比如媒体虎嗅,之前就和微博的关系搞得不是很好,当然微博在自己的注册协议(http://weibo.com/signup/v5/protocol)中就明确的说清楚了:

当然你要说这是“霸王条款”,那就是你的理解了,但是既然人家明确的写出来了,而且你在注册的时候同意了这个协议,别人封杀你的账号就是他按照“规章制度”办事了。

信息的审查

当然我们不能在无凭无据的情况下职责有人在背后审查互联网言论,但是至少在不涉及国家敏感信息下,大厂可能还是会在不经意或者经意的情况下对个人的通信言论进行审查,有一个典型的例子,就是大家在用微信聊天的时候发送图片、特别是截图的时候会感觉明显的非常慢,可能小图标转了半天还是发不出去,我的理解在这个过程中就有“审查”出现了。而且某个管家还会依据“网友”的举报对链接进行分级,加上不同的提示,比如危险、拦截、健康等等。所谓信息的不对称,就是不对称在这里,用别人的手段啊。当然作为一个企业,应该没有一个老板希望自己公司的聊天记录经过了其他人的服务器吧?

为啥要选择Rocket.chat

Rocket.chat是什么

这个东西大家都一致认为是slack的开源代替品。反正项目组也是这么自己介绍自己的。“Have your own Slack like online chat, built with Meteor.”,除了这个,从Google搜到的自己的介绍是Rocket.Chat 是特性最丰富的 Slack 开源替代品之一。 主要功能:群组聊天,直接通信,私聊群,桌面通知,媒体嵌入,链接预览,文件上传,语音/视频 聊天,截图等等。这个项目暂时是托管在github的,项目地址 https://github.com/RocketChat/Rocket.Chat 。

部署到哪

其实项目主页上各大托管厂商也都在互相争抢生意,看排名颇有竞价的意思,大概有这么一些都能快速部署:

方便是方便,但是毕竟咱们在中国,弄到这些环境企业访问起来还是比较麻烦,那国内有没有就自动集成这个的呢?讲了半天终于讲到本文的重点了,除了部署到自己的服务器上,还可以从SAE上快速安装

部署流程

从应用商店安装Rocket.chat

注册一个SAE账号后,进入:http://sae.sina.com.cn/?m=appmarket&a=index&id=5&type=community

点击安装后,选择一个二级域名的名字,然后选下运行环境的配置点下一步即可,这里将会使用到SAE的两个服务,MongoDB和容器运行环境,这两个都是按照运行时间计费的。稍微等待一下就可以等到安装完成界面了。但是由于需要MongoDB服务,这个东西SAE给分配了两个二级域名,但是解析需要一点时间,导致容器第一次启动的时候会报连不上mongodb的错误,稍等两分钟后就会自动启动成功的

创建管理员账户

这个软件的第一个注册用户就是默认的管理员,所以在安装成功后,看到容器启动就可以去创建账号了。

开始使用

到此应该就完成了搭建,看看,整个过程不用写一行代码,点点鼠标就行了。

股票实时接口

6/20号,我们联合新浪财经发布了实时的股票查询接口。这个接口在国内确实算是第一份正规、有保障的数据源了,很值得广大的股票从业人员、开发人员使用。该数据的最大的特点就是百分百保证实时,百分百保证怎么调用都不会封禁。

昨天也拿这个事情发了一个微信的动态,很多朋友对此表示很关心。好久也没有更新博客了,正好借着这个事情说一下我个人的简介以及介绍一下如何使用这个数据源。

接口文档

先给出官方的文档地址:http://www.sinacloud.com/doc/api.html#xin-lang-cai-jing-shi-shi-bao-jia

数据源能做什么

用一个开放的数据能做什么,这个让我想起了11年微博开放平台很火热的时候,那时候我也是一个普通的应用开发者,那时候仿佛大家的想法层出不穷,有用粉丝的头像做图片墙的、有用粉丝做游戏的、有做各种测试的、还有用接口做树洞的等等等。所以在开放一个数据源的情况下,每个人都会有自己的见解,都可以用程序实现自己心中的想法。我能想到的几个点在以下几点:

展示自家公司的K线图 这个是最典型的应用场景了,如果开发者所在的公司是一个上市公司,并且需要在自己的官网上展示自家公司的股票信息,这个时候没什么比找到一个不会出问题的接口最让人省心的的。

给炒股的人开发提示应用 比如说我接下来就会开发一个这样的应用,可以支持让炒股的人在系统中配置自己的持仓列表,并输入买入价,这样就不用一直盯着大盘了,让股民们自己设置一个赔钱的低点和赚钱可以抛出的高点,系统借助这个接口去帮助股民们实时监控这些数据,达到后就发送短信等。

投资分析 当然这个需要一定的数据累计,需要实时的去抓取股票的信息并保存下来,累计到一定量级的时候就可以拿这些数据做分析了。

模拟操盘系统 借助实时的股票报价接口就可以自己开发一个模拟操盘系统了,还可以实时的得到模拟操盘的盈亏值。

如何使用这个接口

注册SAE并创建应用 按设计,用于接口的签名只能用应用的accesskey和secretkey,因此不论你是不是SAE的应用,想要使用这个接口都必须注册SAE(http://sae.sina.com.cn)并创建一个应用。

接口SDK 我写了一个PHP的sdk,需要的可以从这里下载:https://github.com/xiaosier/gofun/tree/master/sae-stock-api-sdk

[翻译]PHP序列化跟踪栈和异常

原文地址:http://fabien.potencier.org/php-serialization-stack-traces-and-exceptions.html

昨天我修复了一个看起来非常奇怪的bug。在这篇文章里,我将描述这个问题,并说明我找到的解决之道,并说明PHP在这个场景的一些表现。

缺陷报告

这个bug首先提到:当试图序列化一个symfony表单实例的时候,一个PDO的异常就会被抛出:

"You cannot serialize or unserialize PDO instances"

这个异常之所以被PDO抛出,是因为POD的实例不能被正确的序列化。

但奇怪的是,sfForm这个类并不依赖于PDO。那这到底是怎么回事呢?

这个问题的本质

经过一番研究,我们发现,这个bug只会出现在用户使用PDO将session信息存储在数据库的时候。因此,我们在不是瞎猜的情况下,试图验证这个表单实例是否在某种程度上和session有关联。sfForm类除了widget、验证类、验证异常类外和其他并没有依赖。因此我们试图在只引入一个widget、一个验证类(validator)和一个验证异常类的情况下复现这个bug。

令人惊讶的是,问题居然出在验证的异常类上。在symfony中,验证的异常类直接继承自PHP的Exception类。

当代码中包含一个PDO的实例时,试图序列化一个异常的实例就能复现这个bug,可以用如下的代码证明:

$dbh = new PDO('sqlite:memory:');

function will_crash($dbh)
{
  // serialize an exception
  echo serialize(new Exception());
}

// this will throw a PDOException
will_crash($dbh);

发生了什么?当PHP序列化一个异常时,除了序列化异常的错误码,异常信息外,还需要包含调用栈

这个调用栈是一个数组,包含了所有这个脚本在这个时刻执行过的所有函数和方法。跟踪信息包含文件名,文件的行数,函数名,以及一个包含所有传递给函数的所有参数的数组。你发现问题了吗?

这个跟踪栈包含了一个PDO实例的引用,当它传递给will_crash()函数的时候,因此当PDO的实例不能被序列化时,PHP试图序列化调用栈的时候一个异常就抛出了。

因此,当一个调用栈中出现一个非序列化的对象时,这个异常都不能被序列化。

解决办法

在PHP中,你可以通过实现一个序列化的接口来覆盖序列化的过程。解决方案可以用如下的代码说明:

class sfValidatorError extends Exception implements Serializable
{
  // class code

  public function serialize()
  {
    return serialize(array($this->validator, $this->arguments, $this->code, $this->message));
  }

  public function unserialize($serialized)
  {
    list($this->validator, $this->arguments, $this->code, $this->message) = unserialize($serialized);
  }
}

serialize()函数应该返回一个表示对象的字符串。在我们的使用场景下, 我们序列化了所有的属性出现跟踪栈。

unserialize()反序列化函数将序列化过的字符串当做参数,应该可以构造出一个对象就像执行了构造函数 __construct()一样。

一些验证测试

到目前为止,一切都很如意。但是为了确保问题已经修复,我还是需要找到写一些测试脚本。但是我又不想在测试中继续依赖PDO。只要能仿造PDO的特性就足够了。我们只需要写一个不能被序列化的类即可:

class NotSerializable implements Serializable
{
  public function serialize()
  {
    throw new LogicException('You cannot serialize or unserialize NotSerializable instances');
  }

  public function unserialize($serialized)
  {
    throw new LogicException('You cannot serialize or unserialize NotSerializable instances');
  }
}

快速搭建一个PHP7学习平台

最近php7已经发布一定时间了,但是这么多新特性总需要大家学习一下才能在自己的项目中或者工作中利用起来,那么怎么搭建一个php7的学习平台呢?新浪云就是一个合适的地方。

php7的新特性

由于本文的重点不是来介绍php7的新特性的,这里就推荐两个PHP官方的介绍链接:

了解新浪云的容器平台

前段时间新浪云上线了容器平台,作为常规runtime(支持php5.3版本、php5.6版本,Python2.7版本,Java1.7版本的补充),终于可以让开发者在容器中自由运行node.js、go等语言环境了。近期又支持了直接上传dockerfile方式的部署。小伙伴们终于可以在容器中部署一个php7的应用了。

如何部署

创建一个容器类的应用

首先登陆SAE的管理面板(没有账号的就先用微博账号登录一下注册啦:))创建一个容器类的应用,如下图所示。

在sae上部署

只需要下载这个http://opensource.changes.com.cn/php7a.zip,然后解压到本地。然后通过git提交到SAE的应用仓库里去就行。看一下其中我写好的dockfile:

FROM skiychan/nginx-php7:latest
COPY index.php /data/www/
COPY code/ /data/www/code/
COPY start.sh /
RUN chmod +x /start.sh
RUN sed -i 's/php-fpm$/php-fpm -F/g' /etc/supervisord.conf && sed -i 's/80;$/5050 default;/g' /usr/local/nginx/conf/nginx.conf

现在只需要把解压后的文件提交到git即可,怎么部署呢?参考下面的步骤吧,进到解压后的目录,点击右键,进入“git bash”【注:windows下没有安装的同学请下载安装git for windows,这里是官网https://git-for-windows.github.io/

在你应用的git代码目录里,添加一个新的git远程仓库 sae
$ git remote add sae https://git.sinacloud.com/APPNAME (这个是你创建的应用名)
$ git add .
$ git commit -am "make it better"
$ git push sae master:1
然后输入您的SAE安全邮箱和安全密码即可,如果忘记的从这里 http://www.sinacloud.com/ucenter/profile.html?from=sidebar 可以查看和修改密码。
后面就可以看到部署中的状态信息,看到最后的OK就完成了。

部署完成后从sae的管理面板,进入应用首页,选择容器管理,应该可以看到如下的页面:

还可以通过web版本的终端进入您的docker容器哦:

这样部署后应该就可以访问了,比如我的应用是php7a,访问的地址是 http://php7a.applinzi.com/code/(一定要有这个/code/路径哦,因为我制作dockerfile的时候特意安装到这个路径了。),把php7a替换为你的应用名即可。

上手练习

这个时候就可以登录这个了,访问 http://php7a.applinzi.com/code/ 可以看到:

默认的用户名和密码都是 admin ,登录后记得修改哦。

这个时候比如我想测试一下PHP7的新特性 <=> 符,可以创建一个文件,在线输入代码:

<?php
$a = 1;
$b = 2;
$c = $a <=> $b;
var_dump($c);

如图所示:

现在就可以通过 http://php7a.applinzi.com/php7.php 访问啦。

注意事项

由于这些在线新建的应用没有提交到git的管理中,因此如果你重启了应用这些文件将会丢失,如果不想丢弃这些文件,你可以通过新建一个共享存储把共享存储映射到/data/www/test 这个目录下,然后在test目录下在线写php 的代码即可,这样重启后就不会丢失文件。

花开的四月

本是花开飘香的四月,似乎在精神和能力增长的重压下有点吐不过气来。最近看了些《怪诞心理学》,觉得挺有意思。想想也觉得挺悲哀,好多的发泄渠道居然一个都不好使了,想想还是在这里多记录一些成长中的感悟比较好,一来不用在社交媒体上展示自己也有透不过气的时候,而来也不用指望在梦里去发泄情绪。

人之所以会感觉到压力,主要是接下来要做的事情可能是自己能力所不及的,这就跟当初在高中做数学试卷的效果一样,那时候从来没有感觉到压力,原因就在于感觉每次的考试不是挑战而是验收。但生活确实不能像应试时那么单一,需要面临来自工作、交际、爱情、家庭各方面的压力。

工作上,由于“机会主义”的作祟,近期负责了远远高于之前开发的事情,背负了重重的绩效压力。昨晚和老大喝酒后在厕所交谈的谈话是互相鼓励大家都要加油,都需要勇气面对未知的挑战。

感情上,我尽可能的避免冲突,但是现在发现并不能,有时心直口快的说出自己的观点后迎来的就是无边的冷战,这确实让人感觉很恼怒,你把她当心里的方舟,希望在你最无助的时候给你力量的时候她却做了把你拉下水的最后一根稻草。不过人都不是完人,说不定别人也把你当做万能的方舟呢。

生活上,母亲对我的依赖远远大于之前,主要可能是她觉得只有向我才能倾诉她对种种事的不满并期望我能一一解决。但是这确实超出了我的能力,在某些事情上我也只能做墙头草,到处调停,避免事情进一步的恶化。所谓解铃还须系铃人,真正解决问题的确实需要当事人自己,只有自己摆正心态了才能积极的面对所遇到的各种事情。

除了嘈杂的生活,还有音乐和泡面:)

用自己的域名访问访问storage资源

到目前为止SAE上的storage还不支持绑定自有的域名,这给需要使用https或者使用自己域名访问存储的附件文件的开发者们出了一个很大的难题。但是如果灵活的使用SAE的appconfig功能和storage的内置API,其实也可以简单的实现。

文件服务器实现原理

所谓知其然不知其所以然,为什么能通过rewrite的方式将storage的访问地址转换成从应用的二级域名输出呢?如果聊到这个话题,那就不得不稍带一下文件服务器的工作流程了。其实这跟大家熟知的HTTP通信协议一样,HTTP在返回的时候会通过【header】和【body】各自带上一部分东西。其中比较关键的是header中的【content-type】,它告诉浏览器下载的东西是个什么类型的文件,【content-length】它告诉浏览器或者其他的终端待下载的文件有多大,大家平时看到的chrome下载的那个圈圈的进度就是这么计算的,完成的比例就是已经下载的文件大小和总的content-length相比得到的百分比。当然服务端如果不输出这个content-length有没有问题呢?当然是没有的,但这是一种很不好的习惯,特别是输出一些比较大的文件例如图片或者音视频时。

上面讲了平时通过浏览器访问资源时的两个基本要素。大家最熟知的就是打开网页了,当然网页html便是其中的一个文件,它的content-type是【text/html】,它的实体就是大家通过【查看源代码】看到的那堆HTML文本。正如下面访问www.baidu.com看到的response header一样,

当然其中还包含很多其他的头例如Set-cookie表示要求浏览器在哪个域名下写下什么cookie等。不再一一讲述。

那么访问一张图片是什么流程呢,其实和上面讲述的访问【百度】的首页并没有任何差别,同样还是返回两个关键的点,一个header中的content-type,告诉访问者下载的是个什么类型的图片,比如是PNG还是JPG,body中包含的是文件的实体。对于图片而言就是一些二进制。那么大家熟知的文件服务器(apache、nginx、ftp服务器、各种云存储)到底在输出文件时干了哪些工作呢?

其实所有的文件服务器基本都是按如下的几个流程响应一次请求的:

  • 将访问的uri映射到本地文件的uri
  • 准备输出文件的header头信息,包括上面所述的content-type、content-length、缓存的策略信息、server信息、cookie等等。
  • 准备文件的实体信息
  • 输出header信息、输出实体信息
  • 刷新缓冲区,把所有的内容发送出去

有了以上的知识储备就可以开始了,那么无外乎以上的几个步骤,首先将输入的url路径映射到本地的文件资源路径;然后准备header头、准备实体,然后将文件发送出去就行了。本文的演示代码以php实现,其他语言的实现可以类似处理。

URI映射

大家都知道SAE storage访问的url是类似【http://skirt-wordpress.stor.sinaapp.com/uploads/2015/12/1.png】这个样子的路径,我们的目标是转换成【http://skirt.sinaapp.com/uploads/2015/12/1.png】这种访问路径的格式。但是我们的代码中明明没有uploads/2015/12/1.png这个路径的文件,那怎么能访问的到呢?这个时候Appconfig(类似于apache的.htaccess)的rewrite功能就派上用场了,有了这个玩意,欺骗世界都不再是梦想,url想怎么转换怎么转换,就算明明是个.html结尾的文件也可以给它转成lmth的:)。

我们通过构造http://skirt.sinaapp.com/域某种特定形式的url,然后将其请求重定向到代码中的一个php文件去处理就可以了。为了和本地的文件不冲突,我们特意构造【http://skirt.sinaapp.com/.storage/uploads/2015/12/1.png】这种路径的文件表示我们这个文件其实是storage中的一个文件而不是本地真实存在的文件。作为写程序的人也能一眼看到这个和代码文件中存在的图片的路径是明显不一致的,因为我们本地根本没有.storage这个目录。那么怎么写这个config.yaml文件呢,下面我写了一个例子,假设我们把所有类似【http://skirt.sinaapp.com/.storage/****】的请求都映射到file_server.php去处理。

// config.yaml 文件内容
name: skirt
version: 2
handle:
- rewrite: if(!is_dir() && !is_file() && path ~ "^/.storage/(.*)$") goto "/file_server.php?__file__=$1"
<?php
// 获取请求的路径
var_dump($_GET['__file__']);

这个时候我们简单请求一个路径【http://skirt.sinaapp.com/.storage/upload/1.txt】,神奇的一幕发生了。发现请求已经交给了file_server.php处理了。看到的效果如下:

从这里我们也可以看到,我们第一步已经大工告成了,因为我们已经从http://skirt.sinaapp.com域中的uri映射到了storage中的路径。

准备header和body

从上文中我们已经将文件的路径成功的映射到了storage中的文件路径,那么接下来我们就需要进行第二步,就是按照文件路径从storage中获取文件的meta信息和实体了。本文为了简单,还是按照上面所述,只需要准备文件的content-type和二进制内容,这些都可以从SAE提供的storage api【storage的使用文档可以参考:http://apidoc.sinaapp.com/class-sinacloud.sae.Storage.html】中获取,请参考以下代码:

<?php
// 需要到SAE对应的应用下创建一个bucket,以下是bucket的名字
$bucket = 'wordpress';
use sinacloud\sae\Storage as Storage;
$instance = new Storage();

// 检查文件是否存在
if (!array_key_exists('__file__', $_GET)) {
        // 参数不正确直接输出404
        header('HTTP/1.1 404 Not Found');
        exit();
}
$file_path = $_GET['__file__'];

// 通过storage的函数判断文件存在否
$file_info = $instance->getObjectInfo($bucket, $file_path);

if (!$file_info) {
        // 文件不存在
        header('HTTP/1.1 404 Not Found');
        exit();
}
// 通过这个大概获取到的文件信息类似于
// array(4) { ["size"]=> int(37428) ["time"]=> int(1431926062) ["type"]=> string(9) "image/png" ["date"]=> int(1455890358) }
// 可以看到此时就取到了文件的content-type和content-length
// 我们直接输出文件的两个头
header(sprintf('content-type: %s', $file_info['type']));
header(sprintf('content-length:%d', $file_info['size']));

$tmp = tempnam(SAE_TMP_PATH, 'tmpfile');
// 获取文件内容
$instance->getObject($bucket, $file_path, $tmp);

// 输出文件内容
echo(file_get_contents($tmp));

// 大工告成啦!

此时访问就可以发现,我们成功的把【http://skirt-wordpress.stor.sinaapp.com/uploads/2015/05/utf8mb4.png】替换成了【http://skirt.sinaapp.com/.storage/uploads/2015/05/utf8mb4.png】,这时候就可以用自己的独立域名或者是https了。从下面的效果看,https也可以使用了!

其他的一些工作

当然本文只是简单的讲述了一下怎么实现, 其实还有很多的细节需要处理,例如需要带上缓存过期的头,这样可以避免频繁的请求消耗服务器的资源。这里就需要带上etag的header头,在处理请求的时候也需要返回304的http code,诸如此类的问题还有很多,希望聪明的读者自行摸索。

实例代码的下载地址

http://skirt.sinaapp.com/tmp_code/tmp_code.zip

新浪容器云入门教程(1)-如何部署一个go web应用

新浪容器云介绍

如果您熟知云计算,也知道云计算有几个分类,那么PaaS你也一定不陌生。但是在docker这种技术出来之前主要还是依赖进程之间的隔离来完成PaaS平台的设计的。举一个通俗的例子,如果我想实现两个网站的隔离,传统的做法肯定是不同的用户启动一个httpd进程来隔离。但是这么整还是得借助cgroup去限制各个进程的cpu使用、内存使用等等,不能让一个用户就把一个服务器的CPU、内存耗尽了。但是SAE的实现是基于请求之间的隔离实现。但是这种方式的好处是统一调度、我们对各种软件的修改例如操作系统的优化、httpd的调优、php的改进等等都能被所用的用户“快速的享受到”我们改进的好处。但是凡事有利就有弊,害处就是牺牲了很多的用户体验,例如:

  • 本地不能写了,直接导致的恶果是诸如discuz等传统的软件没法直接安装了
  • 好多的php函数、Apache的配置文件没法写了
  • 只能使用平台提供的软件版本,例如php的版本、jdk的版本、Python的版本等等

总结起来一句话,大家觉得在享受到极快的部署体验下(可能注册个应用立马就能访问了),还是丧失了一些灵活性。当然这也成为好多用户吐槽的原因。其实各家做相同模式业务的也都知道有这个问题,都会面临用户通过各种渠道质问为啥这个在我本地跑的好好地到你们那就不能用的问题。也是在这种大背景下,我们几经波折的推出了“容器云平台”。那么容器云到底是个什么东西呢?

啥是容器云

其实容器云,顾名思义就是能运行很多很多容器的云平台。而容器既不是大家熟知的jetty容器也不是也不是大家用来装双氧水的容器,就是docker容器。也有很多人将它称为轻量级虚拟机,我觉得这个称谓也是比较合理的,我们不妨叫他“昙花一现的虚拟机”,为什么这么说呢,因为docker这个东西没有“重启”,“关闭”这一说,当你关闭一个启动的docker容器或者重启时,其实是docker daemon又把相同的镜像又启动了一个新的。(以上概念在新浪云容器如此,其他家可能有一些技术能保证上次写的文件还在等等可能不一致。)所以说到这里大家应该知道容器云是个什么东西了,其实就是帮你管理启动好多“一次性虚拟机”的云平台。所以为了达成以上几点,又衍生出了一个概念那就是“镜像”。

什么是镜像

其实好多东西都叫镜像,不论是虚拟机的快照,还是大家安装各种虚拟机时下载的ISO文件等等,都被称为“镜像”,docker的镜像也叫镜像,可以狭隘的理解为docker容器的系统快照,只是这个快照不包含系统运行时产生的文件。

在新浪云容器创建几个容器

讲了以上这么多概念终于能切入到本文介绍的正文了,那就是如何在新浪云容器(以下为了方便,简称为我们自己给自己定的缩写SC2了,应该是Sina Cloud Containers的缩写,为了防止和上海某著名跑车协会撞名,就叫SC2了)。登录http://sc2.sinacloud.com,选择“应用”tab就能看到“创建应用”了。大致可以看到类似以下的界面(我们还在频繁的开发修改中,后来的读者看到的界面可能不一致):

先开始创建一个实例就可以了。这时候就创建好了一个容器,但是为了给大家省钱,默认是不会启动任何容器的。因为容器的计费方式和SAE平台的并不一致,是按容器的启动时间和你选的配置的计价单位计算扣除费用的。

上传我们的代码

为了方便大家先体验,我们在github给大家准备好了入门的示例代码,看这里:https://github.com/sinacloud/go-getting-started。我们将代码先下载到本地来,然后用git上传到我们的应用中,注意:我们的git不知道用户上传证书,所以需要大家自动输入安全邮箱和密码才能提交,但是从我的实际测试中看,需要git 客户端的版本在1.8以上才能弹出来让人输入邮箱和密码,如果你是从centos6默认的yum仓库安装的git,那么你的git版本是有问题的。我们可以执行

[root@vm237147/tmp/f2p2xu8381/src/cow-master]#git version
git version 1.8.3.1

看到当前环境的git版本,当然windows下也是有git的,推荐使用这个:http://pan.baidu.com/s/1geaPw4r

准备好git客户端后,我们需要做一个“选择”了,到底是通过git提交的时候自动就把代码构建为docker的镜像呢,这种比较适合我会在本地一次修改,测试通过后再提交,还有我们也提供异步的镜像构建。意思就是说你可以把git只当一个代码管理的工具使用,从sc2的在线管理平台处就能设定。在这个地方的“勾”:

以下是我通过自动部署时候的命令行样子:

如果git只是提交代码,我们需要去到sc2的在线管理平台部署我们的应用,其实过程都是一样的,只是触发的地方不一样,如果是在界面上完成的,大概是长这个样子的(当然我这部署失败了大家不要在意):

部署后的样子

这时候我们就能看到启动好的容器了,从sc2的管理面板上就能看到容器使用的CPU、内存等,还能看到各种日志。容器的实时状态:

访问日志、错误日志、运维日志的样子:

Godep是啥

写go的都知道我们我们在代码里面会这么写

import "github.com/xx/xx"

其实go在编译的时候又是要求你必须提前go get github.com/xx/xx把代码下载到本地的src的,但是从代码管理的角度说呢,第三方的代码不算是我们工程的代码,不应该被提交到我们的代码管理仓库中。那这种咋办,有了go的包管理工具,新浪云要求必须要有Godep这个东西,可以去:https://github.com/tools/godep 这里下载godep,也可以直接用heroku编译好的版本,加入直接放到/usr/local/sbin/.

wget https://raw.githubusercontent.com/kr/heroku-buildpack-go/master/linux-amd64/bin/godep -O /usr/local/sbin/dodep

如果使用godep自动生成依赖

在你的代码路径下执行:

/usr/local/sbin/godep save -r

就可以看到自动生成了Godeps目录,它下面的文件大概是这样的:

[root@vm237147/tmp/f2p2xu8381/src/cow-master]#ls Godeps
Godeps.json  Readme  _workspace

看看那个json文件吧大概长这样:

[root@vm237147/tmp/f2p2xu8381/src/cow-master]#cat Godeps/Godeps.json
{
        "ImportPath": "cow",
        "GoVersion": "go1.5.1",
        "Deps": [
                {
                        "ImportPath": "github.com/codahale/chacha20",
                        "Rev": "ec07b4f69a3f70b1dd2a8ad77230deb1ba5d6953"
                },
                {
                        "ImportPath": "github.com/cyfdecyf/bufio",
                        "Rev": "9601756e2a6b5fa8ca6749ce4f73f6afdd83030d"
                },
                {
                        "ImportPath": "github.com/cyfdecyf/color",
                        "Rev": "31d518c963d22b95d500ab628c1d1d1b8eff2ab9"
                },
                {
                        "ImportPath": "github.com/cyfdecyf/leakybuf",
                        "Comment": "1.0",
                        "Rev": "ffae040843bee2891b6306d1d085c25ca822e72c"
                },
                {
                        "ImportPath": "github.com/shadowsocks/shadowsocks-go/shadowsocks",
                        "Comment": "1.1.4-4-g2b4d9d7",
                        "Rev": "2b4d9d7c839f2939ec6a96cc423dea44319e848b"
                },
                {
                        "ImportPath": "golang.org/x/crypto/blowfish",
                        "Rev": "f18420efc3b4f8e9f3d51f6bd2476e92c46260e9"
                },
                {
                        "ImportPath": "golang.org/x/crypto/cast5",
                        "Rev": "f18420efc3b4f8e9f3d51f6bd2476e92c46260e9"
                },
                {
                        "ImportPath": "golang.org/x/crypto/salsa20/salsa",
                        "Rev": "f18420efc3b4f8e9f3d51f6bd2476e92c46260e9"
                }
        ]
}

关于Procfile

我们的文档中也简单描述了这个东西,应用可以通过代码根目录下的 Procfile 文件指定在容器中运行的程序命令。Procfile 文件每一行声明一条需要运行的命令,格式如下:

type: command

目前type仅支持 web , 也就是web进程,前端负载均衡的请求会被转发给运行这些命令的容器。假如我们的go编译完后二进制名字叫cow那么就可以这么启动

web: cow

如果要在启动的时候带上参数也是可以的

web: cow -rc=/app/rc

我们上传完的代码在哪儿

因为大家暂时还是不能登录到docker的容器中的,所以难免会有这些疑问,我们的代码传上去到底在哪个路径呢,我应用程序启动的时候要指定配置文件去哪弄?我们应用上传完之后默认会在/app这个目录下,这个目录也是docker容器当前用户的home目录,为了印证这个问题,我写了一个http的文件探针,来列一下我们的文件到底是啥样的,主要的代码在这:

r.POST("/ls", func(c *gin.Context) {
        // check dir exist
        dir := c.PostForm("dir")
        dirList, err := ioutil.ReadDir(dir)
        if err != nil {
            c.String(http.StatusOK, "dir not exist")
        } else {
            var ret string = ""
            for _, v := range dirList {
                ret += v.Name() + "\n"
            }  
            c.String(http.StatusOK, ret)
        }  
    })

大家讲这个方法加到我们github那个实例应用的main.go中重新提交部署就可以了。

先看看我本地的目录:

[root@vm237147/tmp/test2/git]#ls -al
total 48
drwxr-xr-x 7 root      root      4096 2015/12/29 16:46:13 .
drwxrwxr-x 4 mingming6 mingming6 4096 2015/12/28 12:16:58 ..
drwxr-xr-x 2 root      root      4096 2015/12/29 13:45:17 .cow
drwxr-xr-x 8 root      root      4096 2015/12/29 14:53:36 .git
drwxr-xr-x 3 root      root      4096 2015/11/26 19:22:52 Godeps
-rw-r--r-- 1 root      root      3426 2015/12/29 14:52:51 main.go
-rw-r--r-- 1 root      root      1305 2015/12/28 12:17:20 memcache.js
-rw-r--r-- 1 root      root      1661 2015/12/28 12:17:20 mysql.js
-rw-r--r-- 1 root      root        24 2015/11/26 19:22:52 Procfile
-rw-r--r-- 1 root      root       726 2015/11/26 19:22:52 README.md
drwxr-xr-x 2 root      root      4096 2015/11/26 19:22:52 static
drwxr-xr-x 4 root      root      4096 2015/12/28 12:18:35 templates

再来发一个请求看看docker中把我们提交的代码放到哪儿去了。

[root@vm237147/tmp/test2/git]#curl 'http://hmh69m3229.sinaapp.com/ls' -d "dir=/app"
.basher
.cow
.profile.d
.release
Godeps
Procfile
README.md
bin
main.go
memcache.js
mysql.js
static
templates

大家可以看到这个app目录就是我们代码提交后保存的目录,其中还有一个bin就是我们go程序编译完之后的目录,我们列一下看看:

[root@vm237147/tmp/test2/git]#curl 'http://hmh69m3229.sinaapp.com/ls' -d "dir=/app/bin"
go-getting-started
templates

果不其然,我们的Procfile是这么写的

[root@vm237147/tmp/test2/git]#cat Procfile
web: go-getting-started

从bin中的go-getting-started大家应该就能发现了吧,这里的Procfile中的go-getting-started启动的就是这样,所以类比的,大家如果程序是其他名字的就将Procfile改成其他名字,如果要启动配置文件的可以将文件放在代码中提交,然后用go的flag传入,在启动时指定到/app/路径就可以了。

结束语

相信看过这个文章再加上实际的操练应该可以初步搞清楚“云容器”是个什么东西,怎么运行起一个go web,后续的文章将会给大家介绍如果在我们的云容器中使用共享存储,如果使用SAE中MySQL、memcache等服务。祝大家2016新年快乐!

如何调试Apache的400错误

诱发原因

此处我们默认导致400的原因是由于我们写rewrite规则错误导致的400错误。所谓400错误就是不合法的请求,意思是服务端已经不知道你请求一个域名加一个路径的含义了。

解决办法

mod_rewite模块支持很多指令,大家可以参考http://blog.chinaunix.net/uid-20639775-id-154471.html。只需要从其中配置上Apache处理rewrite的过程的日志即可,以我实际碰到的一个bug为例,之前的rewrite规则大概是这样的,通过.htaccess文件。

RewriteEngine On

# Some hosts may require you to use the `RewriteBase` directive.
# If you need to use the `RewriteBase` directive, it should be the
# absolute physical path to the directory that contains this htaccess file.
#
# RewriteBase /

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

这个rewrite规则很简单,意思就是把所有的请求都rewrite到index.php脚本处理,但是实际并不是这样,访问的时候一直报400错误,那是什么原因导致的呢,通过在vhost文件的rewrite模块加日志。参见下面的加法:

<IfModule rewrite_module>
        RewriteEngine On
        RewriteLog /data0/logs/rewrite_log
        RewriteLogLevel 9
    </IfModule>

注意看到RewriteLog和RewriteLogLevel,一定要将其等级设置为9才能看到详细的log,这时候我们让Apache重新加载模块 apachectl -k graceful,然后等待日志可以看到:

10.217.88.69 - - [28/Dec/2015:13:28:20 +0800] [op.admin.sae.sina.com.cn/sid#7f4a8b755ca0][rid#7f4a8bcf3338/initial] (2) [perdir /data1/www/htdocs/op.admin.sae.sina.com.cn/] rewrite 'api/login' -> 'index.php'
10.217.88.69 - - [28/Dec/2015:13:28:20 +0800] [op.admin.sae.sina.com.cn/sid#7f4a8b755ca0][rid#7f4a8bcf3338/initial] (3) [perdir /data1/www/htdocs/op.admin.sae.sina.com.cn/] add per-dir prefix: index.php -> /data1/www/htdocs/op.admin.sae.sina.com.cn/index.php
10.217.88.69 - - [28/Dec/2015:13:28:20 +0800] [op.admin.sae.sina.com.cn/sid#7f4a8b755ca0][rid#7f4a8bcf3338/initial] (2) [perdir /data1/www/htdocs/op.admin.sae.sina.com.cn/] strip document_root prefix: /data1/www/htdocs/op.admin.sae.sina.com.cn/index.php -> /index.php
10.217.88.69 - - [28/Dec/2015:13:28:20 +0800] [op.admin.sae.sina.com.cn/sid#7f4a8b755ca0][rid#7f4a8bcf3338/initial] (1) [perdir /data1/www/htdocs/op.admin.sae.sina.com.cn/] internal redirect with /index.php [INTERNAL REDIRECT]
10.217.88.69 - - [28/Dec/2015:13:28:20 +0800] [op.admin.sae.sina.com.cn/sid#7f4a8b755ca0][rid#7f4a8bcfb7c8/initial/redir#1] (2) init rewrite engine with requested uri /index.php
10.217.88.69 - - [28/Dec/2015:13:28:20 +0800] [op.admin.sae.sina.com.cn/sid#7f4a8b755ca0][rid#7f4a8bcfb7c8/initial/redir#1] (1) pass through /index.php
10.217.88.69 - - [28/Dec/2015:13:28:20 +0800] [op.admin.sae.sina.com.cn/sid#7f4a8b755ca0][rid#7f4a8bcfb7c8/initial/redir#1] (3) [perdir /data1/www/htdocs/op.admin.sae.sina.com.cn/] strip per-dir prefix: /data1/www/htdocs/op.admin.sae.sina.com.cn/index.php -> index.php
10.217.88.69 - - [28/Dec/2015:13:28:20 +0800] [op.admin.sae.sina.com.cn/sid#7f4a8b755ca0][rid#7f4a8bcfb7c8/initial/redir#1] (3) [perdir /data1/www/htdocs/op.admin.sae.sina.com.cn/] applying pattern '^' to uri 'index.php'
10.217.88.69 - - [28/Dec/2015:13:28:20 +0800] [op.admin.sae.sina.com.cn/sid#7f4a8b755ca0][rid#7f4a8bcfb7c8/initial/redir#1] (4) [perdir /data1/www/htdocs/op.admin.sae.sina.com.cn/] RewriteCond: input='/data1/www/htdocs/op.admin.sae.sina.com.cn/index.php' pattern='!-f' => not-matched
10.217.88.69 - - [28/Dec/2015:13:28:20 +0800] [op.admin.sae.sina.com.cn/sid#7f4a8b755ca0][rid#7f4a8bcfb7c8/initial/redir#1] (1) [perdir /data1/www/htdocs/op.admin.sae.sina.com.cn/] pass through /data1/www/htdocs/op.admin.sae.sina.com.cn/index.php

如此就可以看到各种内部的转跳[INTERNAL REDIRECT],快速的定位rewrite规则了。

几种方式在php中实现定时器

使用场景

在用php写逻辑代码的时候一般很少碰到需要严格定时器的场景,因为大家总是希望在一次同步的请求中完成所有的任务,如果不是逼不得已,可能也很少将业务拆分到异步的任务去完成。我认为主要还是自己搭建一套靠谱的、通用的异步任务执行框架比较困难,也比较麻烦。但是现在有很多创意十足的php扩展或者是PaaS平台已经有好的实现了。利用这些工具或者框架构建一套自己的异步任务执行框架就简单的多了。这时候定时器就派上了用场,例如在我们拆分子任务执行的时候将其执行的时间设置为一个固定的值,在任务调度时时如果在设定的时间后没有完成的重新触发,这样就可以保证其反复执行直到成功。下面会提到几种方式分别限制一段php代码的执行时间,各有利弊。

使用max_execution_time

这个可能是大家最熟悉的方式了,通过php.ini能配置的一个参数来限制脚本的最大执行时间。给出测试的代码:

<?php
$i = 1;
while (1) {
    $i++;
    usleep(1);
}

这时候通过命令行执行我们的代码,并通过-d参数限制最大的执行时间就可以看到:

[root@localhost /]# php -d "max_execution_time=1" test.php
PHP Fatal error:  Maximum execution time of 1 second exceeded in /data1/www/htdocs/sae.changes.com.cn/test.php on line 5

很显然是起到了限制的作用,这时候我们稍微修改下测试的代码。看看在这种强执行时间限制之下能不能执行我们设置的register_shutdown_function,测试代码如下:

<?php
function shutdown()
{
    echo 'success exec';
}
register_shutdown_function('shutdown');

$i = 1;
while (1) {
    $i++;
    usleep(1);
}

执行测试代码可以看到:

[root@localhost /]# php -d "max_execution_time=1" test.php
PHP Fatal error:  Maximum execution time of 1 second exceeded in /data1/www/htdocs/sae.changes.com.cn/test.php on line 11
success exec

太好了,说明这种方式做执行时间的限制还不赖。但是还是有些不方便的地方,这主要在于会同安全组的策略相违背,至少我们在php安全的设置的初期是把ini_set函数屏蔽了的。这时候使用这种方式可能会在部分云平台上会出现问题。

使用php中的declare ticks指令完成

这个东西可以简单的理解为php为开发者为基本的执行单元执行后加了可以hook的函数,具体的介绍可以参考php的文档http://php.net/manual/zh/control-structures.declare.php,先看例子和执行结果:

<?php
declare(ticks = 1);
function shutdown()
{
    echo "safe shutdown \n";
}
register_shutdown_function('shutdown');
define('max_exectime', 1);
$time_start = microtime(true);
var_dump($time_start);
function check_exec_time()
{
    global $time_start;
    $time_now = microtime(true);
    if ($time_now - max_exectime > $time_start) {
        var_dump($time_now);
        echo "bye";
        exit();
    }

}
register_tick_function('check_exec_time');

for ($i = 0; $i < 10000100; $i++) {
    $i++;
    usleep(1);
}

我们可以看到执行的结果:

[root@localhost /]# php tick.php
float(1442126693.6926)
float(1442126694.6926)
byesafe shutdown

可以看到借助使用ticks指令实现定时器比max_execution_time的好处在于前者可以轻松实现毫秒级别的定时器但是max_execution_time最小的粒度只能到秒级别。另外这种方式不会和大部分的安全策略相违背。但是缺点也是很明显了,指定的周期设置的越短定时越精确,但是对性能的损耗也越大,因为总要跳出来去检测时间。

swoole的定时器

使用过swoole扩展的同学肯定知道这个扩展也提供了毫秒定时器的功能,具体可以参考http://wiki.swoole.com/wiki/page/174.html。当然在使用时必须要编译安装swoole扩展,给出测试代码:

<?php
var_dump('swoole version:'.swoole_version()."\n");
swoole_timer_tick(1000, 'exec_function');
swoole_timer_after(1, function(){
     echo(1);
});

function exec_function()
{
    // 这里直接退出了
    echo "timer exec\n";
    var_dump(getmypid());
    exit();
}

这时我们执行可以得到结果:

[root@localhost /]# php swoole.php
string(22) "swoole version:1.7.19"
1timer exec
int(19908)

可以看到1000毫秒的定时器工作了,swoole_timer_after的回调函数中用于执行需要执行的代码逻辑,这种用法有许多的依赖,首先swoole在实现的时候使用了timerfd,但是这个东西在内核版本2.6.25后才加入,如果内核版本低于这个应该是无法工作的,另外swoole的定时器需要在“全异步”的环境中才能工作,暂时不能用于apache等环境中,不能使用usleep、sleep函数阻塞进程,否则就无法工作了。

mysqli_set_charset不一定靠谱

起因来自最近测试MySQL的宽字符支持,也就是使用4字节存储,但是其utf8编码单字符应该最多只支持三Bytes。具体可以参考https://dev.mysql.com/doc/refman/5.7/en/charset-unicode-utf8.html,这里需要明确一下MySQL的utf8和标准的UTF-8在拼写上都是不一样的,标准的定义是:

Unicode Transformation Format with 8-bit units

按照MySQL的文档写的,以下语言分别用到这些个bytes位数,

  • Basic Latin letters, digits, and punctuation signs use one byte.单个的拉丁字母、数字、英文符号
  • Most European and Middle East script letters fit into a 2-byte sequence: extended Latin letters (with tilde, macron, acute, grave and other accents), Cyrillic, Greek, Armenian, Hebrew, Arabic, Syriac, and others.主要是一些单词
  • Korean, Chinese, and Japanese ideographs use 3-byte or 4-byte sequences.韩语、中文、日语等。

而MySQL的utf8默认是3-Byte UTF-8 Unicode Encoding,它也有个别名就叫The utf8mb3 Character Set。问题这时候就出来了,我们要想MySQL能支持4bytes的存储,那么编码就不能使用utf8了,其实MySQL也是支持4bytes编码的,但这个时候字符集不叫utf8了换了个名字叫utf8mb4。英文叫4-Byte UTF-8 Unicode Encoding。

继续测试,我们配置了MySQL服务端,让它支持了这个4位的编码utf8mb4,但是从客户端连接却惊奇的发现还是不支持,当时我是震惊了,后来一想我们用了MySQL的代理是不是代理不支持呢,然后从原生的环境连接代理发现可以写进去,那问题就是我们改过的非原生环境了。(我们的原生环境分为php5.3的版本和php5.6的版本)。

这么一看就是MySQL客户端的问题,没想到问题还会出现在它身上,之后一路寻找,发现使用了mysqli_set_charset这个函数,但是却并没有成功的设置编码。我们找到php的源码,这么写的

PHP_FUNCTION(mysqli_set_charset)
{
        MY_MYSQL        *mysql;
        zval            *mysql_link;
        char            *cs_name;
        size_t          csname_len;

        if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Os", &mysql_link, mysqli_link_class_entry, &cs_name, &csname_len) == FAILURE) {
                return;
        }
        MYSQLI_FETCH_RESOURCE_CONN(mysql, mysql_link, MYSQLI_STATUS_VALID);

        if (mysql_set_character_set(mysql->mysql, cs_name)) {
                RETURN_FALSE;
        }
        RETURN_TRUE;
}

可以看到直接调用了libmysql的mysql_set_character_set,PS我们的MySQL的驱动是libmysql,然后就看mysql_set_character_set了,函数原型是int mysql_set_character_set(MYSQL *mysql, char *csname),这样的该函数用于为当前连接设置默认的字符集。字符串csname指定了1个有效的字符集名称。连接校对成为字符集的默认校对。该函数的工作方式与SET NAMES语句类似,但它还能设置mysql->charset的值,从而影响了由mysql_real_escape_string()设置的字符集。该函数是在MySQL 5.0.7中增加的。该函数0表示成功,非0值表示出现错误。

从测试的代码中打印出php调用的结果却是是false,那是为啥呢?还是看下实现吧https://github.com/mysql/mysql-server/blob/09ddec8757b57893ccd2f2c2482b3eec5ca811e5/sql-common/client.c#L5898这时候具体寻找的细节我就不展开说了,但是值得一说的是说明文件中有支持的所有编码,如果是个rpm包的方式安装,可以像这么执行

rpm -ql mysql-libs-5.1.73-3.el6_5.x86_64

出来所有的文件要寻找的是/usr/share/mysql/charsets/Index.xml这样子的,这个xml里面有libmysql客户端支持的所有编码(在你安装的版本下),这时候我们vim打开以下,查找。先找个utf8试试,果然是有的

<charset name="utf8">
  <family>Unicode</family>
  <description>UTF-8 Unicode</description>
  <alias>utf-8</alias>
  <collation name="utf8_general_ci"     id="33">
   <flag>primary</flag>
   <flag>compiled</flag>
  </collation>
  <collation name="utf8_bin"            id="83">
    <flag>binary</flag>
    <flag>compiled</flag>
  </collation>
</charset>

再找utf8mb4,果然找不到

所以看到这时候是libmysql客户端的问题,怎么办呢,那就乖乖用SET NAMES 执行编码了, 看到别人写了好多文章巴拉巴拉还说什么优劣的,我觉得没啥区别,反而set names在通用环境下更合适,反正就告诉服务端我给你什么编码,你也给我返回什么编码就好了。