Nginx https load-balance on Raspberry Pi

我的Raspberry Pi工作了大约半年之后,文件系统开始出现莫名其妙的问题,各种文件都出现乱码,以至于某系统命令一执行就segment fault。到重新“安装”系统的时候了。

之前主要跑的服务是我的博客,nginx + wordpress搭的,在RPi上因为CPU的问题(php-fpm执行时几乎占满cpu),响应比较慢。既然重新弄了,考虑把某些请求交给家里的其它机器来处理。
家里还有一台跑Ubuntu的笔记本,希望满足如下要求:
1) 如果笔记本开机着,http请求(几乎)都交给笔记本处理;
2) 如果笔记本关机,http请求都由RPi来处理;
这些应该是完全自动实现的。

在Stackoverflow上问了这个问题,有人提示说可以通过nginx的load-balance功能来实现我的需求。于是开始google一番,找到了解决方案。
基本的思想是,把nginx配置成反向代理,笔记本和RPi都作为upstream来处理http请求;通过nfs共享同一个wordpress目录;配置mysql让wordpress访问同一个数据库。
一步一步来配置这些。
其中Raspberry Pi的地址固定为192.168.1.100,笔记本的地址固定为192.168.1.101,数据库名为minewpdb,数据库用户名为minedb

1. 配置Mysql
mysql仍然放在RPi上,让mysql接受网络的请求,这样不同的机器可以用同一个mysql数据库。

sudo vim /etc/mysql/my.cnf
==> 把 bind-address 改成RPi的ip地址
bind-address            = 192.168.1.100

mysql -u root -p #登录mysql控制台
mysql> grant all on minewpdb.* to 'mineblog'@'192.168.1.100' identified by 'xxx'; # 本地RPi的wordpress访问
mysql> grant all on minewpdb.* to 'mineblog'@'192.168.1.101' identified by 'xxx'; # 笔记本的wordpress访问
mysql> quit;
sudo service mysql restart # 重启mysql服务

# 在RPi和笔记本上分别测试一下
mysql -u mineblog -h 192.168.1.100 -p # 如果能登录成功,就ok了

2. 配置wordpress
Wordpress仍然在RPi上,只要修改wordpress的config文件,把数据库的host,用户名和密码都设对就行了。

sudo vim /path/to/wordpress/wp-config.php
==> 修改DB_HOST等参数
define('DB_NAME', 'minewpdb');
define('DB_USER', 'mineblog');
define('DB_PASSWORD', 'xxx');
define('DB_HOST', '192.168.1.100');

3. 配置nfs
在RPi上设置nfs export,让笔记本mount它,这样可以保证跑的wordpress是同一份。


sudo vim /etc/exports
==> 添加wordpress目录到exports
/path/to/wordpress 192.168.1.101(rw,no_root_squash,insecure,sync,no_subtree_check)
sudo service rpcbind start  # RPi的nfs依赖于rpcbind
sudo update-rc.d rpcbind enable  # 设置rpcbind自动启动
sudo service nfs-kernel-server start  # 重启nfs

在笔记本上,mount这个目录:

sudo mount -t nfs 192.168.1.100:/path/to/wordpress /path/to/wordpress

4. 配置nginx
在RPi上要把nginx配置成反向代理,upstream是RPi和笔记本,端口号都是8000,真正的wordpress运行在8000端口上。
添加新的配置文件 /etc/nginx/sites-available/wordpress-load-balance,注意我把http的访问全部重定向到https,以防GFW

# Upstream to abstract backend connection(s) for php
upstream php {
server unix:/var/run/php5-fpm.sock;
}

upstream mineservers {
# 设置两个upstream, 其中笔记本优先,同时设置5s的fail_timout
# 因为局域网很稳定,所以max_fails设成1就行了,
# 如果fail就说明笔记本关机中,让RPi自己来处理
server 192.168.1.101:8000   weight=999 fail_timeout=5s max_fails=1;
server 192.168.1.100:8000;
}

server {
listen 80;
server_name mine260309.me;
rewrite     ^ https://$server_name$request_uri? permanent;
}

server {
listen          443 ssl;
server_name     mine260309.me;

ssl_certificate     /path/to/cert/mine260309.me.2014.chain.crt;
ssl_certificate_key /path/to/cert/mine260309.me.2014.key;
ssl_protocols       SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers         HIGH:!aNULL:!MD5;
access_log         /path/to/wordpress/logs/proxy.log;
error_log            /path/to/wordpress/logs/proxy_error.log;

location / {
# 代理到upstream
proxy_pass  http://mineservers;

### force timeouts if one of backend is died ##
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;

### Set headers ####
proxy_set_header        Accept-Encoding   "";
proxy_set_header        Host            $host;
proxy_set_header        X-Real-IP       $remote_addr;
proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;

### Most PHP, Python, Rails, Java App can use this header ###
#proxy_set_header X-Forwarded-Proto https;##
#This is better##
proxy_set_header        X-Forwarded-Proto $scheme;
add_header              Front-End-Https   on;

### By default we don't want to redirect it ####
proxy_redirect     off;
}
}

server {
root /path/to/wordpress;
# 在8000端口监听
listen          8000;
server_name     mine260309.me;
... # normal wordpress configurations
}

在笔记本上用同样的配置就可以了。

这样,任何一个请求到RPi,它会让nginx优先反向代理到笔记本的nginx的8000端口,由笔记本来处理;
如果笔记本关机,在5秒钟之后它会fail,于是再由RPi自己的8000端口处理。

Q.E.D.

Share

蜜月马尔代夫

  新婚蜜月,目的地早就选好了马尔代夫。各种论坛上一番搜索和比较之后,在携程上订了Banyan Tree Vabbinfaru(瓦宾法鲁悦榕庄)7天5晚的行程.

· 超级漂亮的一个小岛,仿佛是漂浮在蔚蓝清澈的海水中;纯白色的沙滩,脚感很细腻;住的观海别墅也很精致,遮阳伞,小泳池,按摩温泉,户外冲澡,有点小奢侈的享受。
   
  悦榕庄的服务也不错,每个人见到了都会微笑的打声招呼,对于honeymoon还有特别的服务,一个装满各种见过没见过的水果的欢迎果篮;一瓶红酒;还贴心地做了一个心型的花瓣床,给了lp一个惊喜~

· 沙滩螃蟹
  沙滩上有一种白色半透明的小螃蟹,有了保护色有点难发现,不过动的时候还是很容易“暴露”足迹;这种小螃蟹到了晚上还会像萤火虫一样发出光亮,很神奇。

        你能看出螃蟹在哪儿吗?Smile

· 浮潜
  浮潜的装备完全可以不用带,因为可以免费借。刚到岛上我就很兴奋地自己去尝试了,结果喝了好几口海水,咸极了。于是和老婆一起上了节浮潜课,一个昵名为“Jackie Chan”的老师给我们示范,我很快就学会了,老婆有点害怕,跟着老师(或者说被老师推着)去远处的珊瑚礁游了一圈。
  真的有很多鱼!以前真没想过能在身边看到这么多鱼,什么颜色的都有(虽然在海里面颜色没有那么鲜艳),甚至有一大群小鱼从身边穿游而过,这种感觉真是美妙!
  没有穿长袖长裤,就算是涂了防晒霜,还是顶不住那太阳,浮潜一两个小时,顿时就晒黑了。。。不过显得还是蛮健康的哈
  以后再浮潜一定会想要带个水下相机的,这样的美景不拍下来真是浪费啊!

· 飘浮
  当年看到过马尔代夫的一张人漂浮在海面上的照片,当时就觉得我一定也要这么漂一次!岛上有这样的橡胶垫,于是,我们也能这么漂啦~
 

· 星空,银河
  马尔代夫的空气很干净,虽然海拔基本为0,但晚上还是能看到无数的星星,包括牛郎,织女星,以及银河!既然背了三角架过来了,自然不能浪费这样的好机会,正好学学怎么拍星空。
  带了两个镜头,套头18-105mm,50mm 1.8定焦。
  50定焦的镜头虽然光圈大,但能取景的星空范围太小了,拍不出银河的感觉来,不过用ISO1600,曝光10s,效果还是可以的;
  18-105用18mm焦距,F3.5,手动对焦到无穷远,ISO 1250~2500,曝光30s,也能拍出银河的效果来,当然,还是有点糊的。
  没有赤道仪,没有快门线的情况下能拍到这个效果,也算满意了。。。


    俺拍的银河~

· SPA
  瓦宾法鲁悦榕庄的一大特色就是spa,这也是lp一直很期待但又纠结的项目:期待是因为这边的spa据说很好,纠结是因为这边的spa很贵——基本上单人的全身90分钟spa都要$200以上,好多都要300~500刀。然后我们“碰上”了某个特价的时段,5折,双人全身90分钟,一共210刀,lp顿时没了抵抗力^_^
  我以前没做过spa,没法评价,就是觉得很舒服;lp作为“专业人士”,评价是:泡脚时用了鲜橙子;会问要不要精油按摩还是无油按摩;手法,力度都很好;还有新鲜的姜茶和柠檬茶喝。一切都很好很专业,就是贵。。。
 

· 日出日落
  某一天想看日出,google一下得知是6:03日出,于是定好闹钟。到时起床,发现天还是黑的,有点纳闷,走到东边的沙滩,完全没有日出的样子嘛。然后一拍脑袋发现不对,岛上的时间比马累要早一个小时,马累的6:03是岛上的7:03,起早了一个小时,杯具。。。然后睡了一个小时之后再起来看日出,发现了东边都是云,只能看着太阳把云照耀得很灿烂。。。
  另一天坐着船去Vabbinfaru的姐妹岛Ihuru岛(这个岛更小,Ferry2小时一班,开5分钟就到了),正好是傍晚看日落。走到西边的沙滩,夕阳照耀下的云显得特别好看。

  lp情不自禁地对着夕阳跳了起来~正好遇到一个老外mm,主动帮我们拍照,于是就有了这样的照片~

  最后,来几张和lp的合照吧~
   

Share

用Calibre下载网页专题内容(非RSS Feed) 以及Referer的处理

神器Calibre有个很强大的Fetch News功能,可以下载RSS Feed然后生成电子书推送给Kindle。关于如何用recipe下载RSS Feed网上已经有不少相关的资料了。

但是如果想下载某一个专题,而这个专题又没有RSS输出怎么办呢?比如说:寻找太阳系的疆界

topic_outer_planets

答案是,recipe还有更逆天的手动parse index功能,正好用来分析一个单独的网页,提取出我们想要的文章。

这篇博客将会简要介绍这个功能。

另外,由于某些网站的图片会要求有Referer的HTTP header,如果没有下载时会403 Forbidden,所以下载图片里还要特别注意处理Referer。

Background:

Calibre内置了Fetch News功能,本质上是python实现的一个继承自 calibre.web.feeds.news.BasicNewsRecipe 的类,并且override了一些成员/函数的实现,以实现定制的功能。

不过一般来说所有的News都是通过抓取feed来实现的,比如说内置的“人民日报”的实现:

class AdvancedUserRecipe1277129332(BasicNewsRecipe):
  title          = u'人民日报'    # 标题
  oldest_article = 2    # 最近几天之前的文章
  max_articles_per_feed = 100    # 最大的文章数
  ...
  feeds          = [
    (u'时政', u'http://www.people.com.cn/rss/politics.xml'),
    (u'国际', u'http://www.people.com.cn/rss/world.xml'),
    (u'经济', u'http://www.people.com.cn/rss/finance.xml'),
    (u'体育', u'http://www.people.com.cn/rss/sports.xml'),
    ...
  ]    # 定义具体的feed名称和url
  keep_only_tags = [...]    # 抓取到的网页里保留的内容
  remove_tags = [...]    # 抓取到的网页里去除的内容
  remove_tags_after = [...]    # 抓取到的网页里去除这个tag之后的内容
  ...

一般来说抄一下Calibre内置的recipe,定义以上的变量就可以搞定绝大部分RSS Feed了。

那么如何搞定像“寻找太阳系的疆界”这样的没有RSS输出的专题文章呢?

Solution:

查看 Calibre的API Document,注意到parse_index()这个函数,它说:

This method should be implemented in recipes that parse a website instead of feeds to generate a list of articles.<br />
...
It must return a list. Each element of the list must be a 2-element tuple of the form ('feed title', list of articles).<br />
Each list of articles must contain dictionaries of the form:
{
 'title'       : article title,
 'url'         : URL of print version,
 'date'        : The publication date of the article as a string,
 'description' : A summary of the article
 'content'     : The full article (can be an empty string). Obsolete
 do not use, instead save the content to a temporary
 file and pass a file:///path/to/temp/file.html as
 the URL.
}

并且提到:

For an example, see the recipe for downloading The Atlantic.

也就是说,可以参考内置的 The Atlantic的recipe,它是parse一个website而不是一个feed。打开这个recipe:

class TheAtlantic(BasicNewsRecipe):
  title      = 'The Atlantic'
  __author__ = 'Kovid Goyal and Sujata Raman'
  description = 'Current affairs and politics focussed on the US'
  INDEX = 'http://www.theatlantic.com/magazine/toc/0/'
  language = 'en'
  remove_tags_before = dict(name='div', id='articleHead')
  remove_tags_after  = dict(id='copyright')
  remove_tags        = [dict(id=['header', 'printAds', 'pageControls'])]
  ...
def parse_index(self):
  ... # Parse the webpage and return feeds

很清楚,主要实现了parse_index这个函数。

那么我们就可以参考这个实现,来写自己parse changhai.org的代码。

Step1 实现parse_index

直接看code吧,注释很容易明白:

def parse_index(self):
  articles = []  # 文章列表
  feeds = []  # 最终要返回的
  soup = self.index_to_soup(self.INDEX)  # 获取网页完整的内容
  feed_title = self.tag_to_string(soup.find('title'))  # 网页的标题
  self.log.debug("Feed Title: " + feed_title)
  for table in soup.findAll('table', attrs={'class':'inside-article-noborder'}):
    # Main articles
    for post in table.findAll('td'):  # 文章列表都在 inside-article-noborder 的 table 里,每一个td是一项
      a = post.find('a', href=True)  # 取出链接
      title = self.tag_to_string(post)  # 文章标题
      url = a['href']
      if "#" not in url:  # 如果url里包含"#",其实是指向了同一个网页的某个anchor,忽略这样的项
        self.log.debug("Article title and url: ")
        self.log.debug(title + ": " + url)
        url = self.INDEX + url  # 生成完整的url
        articles.append((
          {'title':title,
           'url':url,
           'description':'',
           'date':''}))  # 添加到文章列表里
    for post in soup.findAll('p', attrs={'class':'center'}): # 术语简介 | 参考文献 在 class: center的p里面
      for a in post.findAll('a', href=True):  # 同理取出文章的标题和url
        title = self.tag_to_string(a)
        url = a['href']
        self.log.debug("Article title and url: ")
        self.log.debug(title + ": " + url)
        url = self.INDEX + a['href']
        articles.append((
          {'title':title,
           'url':url,
           'description':'',
           'date':''}))
  if articles:
    feeds.append((feed_title, articles))  # 最后生成feed,这里只有一个feed,包含多篇文章
  return feeds

这样每篇文章都能被download下来了,下面要处理每篇文章的内容。

Step 2 处理正文内容

分析正文的网页内容,会发现:

正文内容都在 main-body 的table里,所以

keep_only_tags = [
  dict(name='table', attrs={'id':['main-body']}),
]

“返回目录 | 下一篇”这样的link可以忽略,都在 class left 和right的p里面

remove_tags = [
  dict(name='p', attrs={'class':['left', 'right']}),
]

“站长近期发表的作品”及以后的内容应该忽略,它位于article-data-claim的后面,所以

remove_tags_after = [
  dict(name='p', attrs = {'class' : ['article-date-claim']}),
]

好了,到这一步,文章的内容也能完美提取出来了。

但是,正文里的图片还是没有显示,原因在calibre的log里:

Could not fetch image  http://www.changhai.org/articles/science/astronomy/outer_planets/images/kepler_platonic.png
reply: 'HTTP/1.1 403 Forbidden\r\n'

webserver直接返回403错误了,这一般是因为图片下载时没有Referer的HTTP header,算是一算防盗链的功能吧。

所以我们还需要处理Referer。

Step3 在HTTP Request里添加Referer

通过Google,发现可以通过自定义browser.open_novisit这个函数来实现添加HTTP header

def get_browser(self):
  br = BasicNewsRecipe.get_browser(self)
  orig_open_novisit = br.open_novisit
  def my_open_no_visit(url, **kwargs):
    req = mechanize.Request( url, headers = { 'Referer': self.INDEX, }) # 添加Referer的header
    return orig_open_novisit(req)
  br.open_novisit = my_open_no_visit # 把open_novisit这个函数重新定义成自己的函数
  return br

理论上这样就OK了,然后实际运行的时候会发现,下载文章和图片的时候,根本就没有调用到my_open_no_visit(),图片仍然下载失败。

这下就只能看Calibre的源代码了——通过代码和打log,注意到_fetch_article()里面会调用clone_browser(),而这个clone完的browser的open_no_visit函数还是原生的,并不是自定义的函数。假如这个不是bug,就说明俺的recipe是有点exotic了:

Clone the browser br. Cloned browsers are used for multi-threaded downloads, since mechanize is not thread safe. The default cloning routines should capture most browser customization, but if you do something exotic in your recipe, you should override this method in your recipe and clone manually.

好吧,那就自己实现一个clone_browser()吧,同时也更新自己的get_browser():

_cachedBrowser = None  # 自己的browser
def get_browser(self):
  if self._cachedBrowser is None:
    br = BasicNewsRecipe.get_browser(self)
    orig_open_novisit = br.open_novisit
    def my_open_no_visit(url, **kwargs):
      req = mechanize.Request( url, headers = { 'Referer': self.INDEX, })
      return orig_open_novisit(req)
    br.open_novisit = my_open_no_visit
    self._cachedBrowser = br  # 保存browser
  return self._cachedBrowser
def clone_browser(self, br):
  if self._cachedBrowser is None:
    raise Exception("No browser")
  br = BasicNewsRecipe.clone_browser(self, br)
  br.open_novisit = self._cachedBrowser.open_novisit  # 设置clone出来的browser的open_novisit函数
  return br

OK, 这下Referer的问题也解决了,生成的文章里也完美包含了图片。

kindle_outer_planets kindle_outer_planets_2

Finally:

完整的code放在github(见calibre_recipes)上了,当然code还有待完善,比如说可以合并common的code,以后再慢慢改了。

目前实现了3个专题的recipe:

Q.E.D.

p.s. 顺便推荐一下卢昌海老师的书吧,上面的这些文章都已经出版了,并且有的也有Kindle的版本,推荐购买 🙂
p.p.s 在微博上发现@敲代码的张洋已经有了一些抓取网页的Recipe,包括著名的MIT的SICP,以及O’Reilly的开放书籍,很不错。代码也在Github

Share

Kindle Paperwhite 添加中文字体

入手了国行的Kindle Paperwhite,相比Kindle 3有了更好的中文字体支持,原生的“宋体“效果已经相当不错了。
不过以前按照《Kindle 中文阅读终极优化指南》看习惯了“方正准雅宋”,KPW自带的宋体显得有点淡,于是我又开始折腾字体了。。。

The Precondition
首先,KPW添加字体是很简单的一件事情,也不需要越狱:

  1. 用USB线连接KPW到电脑;
  2. 在Kindle的根目录上创建一个空文件 USE_ALT_FONTS (注意不要有扩展名);
  3. 在Kindle的根目录上创建目录 fonts;
  4. 把想要的字体拷到那个fonts目录,如果字体是一个系列的,粗体/斜体按如下命名:
    • Fontname-Regular.ttf     # 普通
    • Fontname-Italic.ttf          # 斜体
    • Fontname-Bold.ttf          # 粗体
    • Fontname-BoldItalic.ttf   # 又粗又斜
  5. 拔掉USB线,重启KPW (菜单->设置->菜单->重启);
  6. 在字体菜单里选择上面添加的字体.

The Problem
但是如果用《优化指南》里提到的这四个字体:

会发现,KPW会把它们认成4个不同的字体,选择“方正准雅宋”之后,也没办法自动使用对应粗体。其实这也很正常,上面的四个字体根本就不是一个Font Family的,KPW自然不认了。

The Solution
解决办法是,用字体编辑器把它们变成同一个Font Family!
在Ubuntu下我试过FontForge, TTFEdit, Ttx这些工具。
FontForge功能太强而且似乎生成的font文件会有问题;
TTFEdit对utf8或者gbk编码的字符串支持有问题;
Ttx其实是一个把TTF转换成xml文件,然后把xml文件转回成ttf的工具。

最后我用ttx搞定了。
1.

ttx -o CJK_Regular.ttx CJK_Regular.ttf  # 把ttf转换成ttx文件(本质就是个xml)

2.

编辑CJK_Regular.ttx
在<name>里,nameID=1是FontFamily,把它改成FZYaSong-GBK
在<OS_2>里,usWeightClass是指字体的Weight,把它改成500 (Medium),顺便把bWeight改成“6”(也许这个改动没有必要)

2.x.

同样的,把另外三个ttx文件里的FontFamily都改成FZYaSong-GBK
CJK_Bold.ttx,CJK_BoldItalic的usWeightClass改成700,bWeight改成8;
CJK_Italic的usWeightClass改成400,bWeight改成6;

3.

ttx -o FZYaSongMod-Regular.ttf CJK_Regular.ttx # 重新生成ttf字体
 另外三个文件同理

这样就生成了FZYaSongMod的四个ttf文件,FontFamily都是FZYaSong-GBK, Weight都是对应的值。然后把这四个文件拷到KPW的fonts文件夹里,重启,搞定!
KPW的显示效果终于和Kindle3一样了,内牛满面啊…

The Last
这四个文件已经打包好,有需求的可以直接用。下载在此: FZYaSongMod.tar.bz2
声明: 更改字体信息仅限学习用,不得作为商业用途。

p.s. 感谢@wzyboy同学《优化指南》 本文修改的字体来源于他的博客。
p.p.s. 推荐越狱之后安装kindlepdfviewer,完美集成于KPW,看PDF效果更好~

Share

婚了

今年5.19 & 5.20,都是好日子。

前者是黄道吉日,办婚礼的人很多,我也在这一天凑热闹;后一天是520,也是南京各大高校的校庆日,我在这一天领了红本本。

如果没有那么多繁文缛节人情世故,结婚是一件很有意思的事情——可以和爱人一起,跑到一个从没去过的地方,旅游结婚。

不过这只是幻想。

实际上是,结婚的前两天先回家,然后忙各种事情:包喜糖盒,买花,理发。。。

然后,然后,就到结婚日了。

妈妈请的婚庆公司不错,布置得很漂亮;司仪是个帅哥,声音不错,不过只是在仪式前跟我们沟通了一下,所以婚礼上基本上是自由发挥了,好在效果还行。

不过婚宴实在太忙了,“表演”完,LP换好造型,就开始敬酒,敬完酒,基本上就已经结束了。

感谢各位同学,跑到我们镇上这么个偏僻的地方来参加我的婚礼,非常感谢哈~

婚后第一天,520,去惠山区民政局领了红本,人果然不少,看来大家确实喜欢这样的日子~

结个婚,很累,不过更多的事情都是老妈在操心,我已经算得上轻松的了,只好在心里对老妈说声感谢了。

总之,感谢大家。俺婚了!

Share