小子 发布的文章

再谈casperjs截图

遇到了什么问题

一直在使用 casperjs 来做网页截图,http://tool.lu/article

简单的使用是没有问题的,当要给墙外网址截图的时候,那么问题来了。

触发点:

  1. phantomjs 使用 proxy
  2. 要截图的网址是 https 的

现象:
被截取的网页会跳转到about:blank,导致截图空白

Eh, what the fuck?!

怎么解决的

最主要的是 --ssl-protocol=any --ignore-ssl-errors=true

基于之前那些个恶心的问题,然后就有了下面这么长的命令:

LC_CTYPE=en_US.UTF-8 PATH=/usr/local/phantomjs/bin:/usr/bin/usr/local/node/lib/node_modules/casperjs/bin/casperjs --proxy=127.0.0.1:7070 --proxy-type=socks5 --ssl-protocol=any --ignore-ssl-errors=true /data/jobs/webshot/webshot.js --url='https://url/you/want/to/capture/' --target='path/to/store/the/picture.png'

实时的 CPU 占用显示

预览

QQ20141108-1.png

流程

  1. golang 分析 vmstat 1 -n
  2. publish 到 redis
  3. subscribe redis 然后通过 SSE push 到 浏览器

代码

performance.go

go get gopkg.in/redis.v2
package main

import (
    "bufio"
    "fmt"
    "log"
    "io"
    "os/exec"
    "strings"
    "strconv"
    "gopkg.in/redis.v2"
)

var client *redis.Client

func tail(stream io.Reader) {
    scanner := bufio.NewScanner(stream)
    scanner.Scan() // 跳过header1
    scanner.Scan() // 跳过header2
    for scanner.Scan() {
        text := scanner.Text()
        segments := strings.Fields(text)
        if ide, err := strconv.ParseInt(segments[14], 10, 32); err == nil {
            used := 100 - ide
            log.Println(used)
            pub := client.Publish("performance", fmt.Sprintf("%d", used))
            if err := pub.Err(); err != nil {
                log.Println(err)
            }
        }
    }
    if err := scanner.Err(); err != nil {
        // ...
    }
}

func main () {
    client = redis.NewClient(&redis.Options{
        Network: "tcp",
        Addr: "127.0.0.1:6379",
    })
    outReader, outWriter := io.Pipe()
    cmd := exec.Command("vmstat", "1", "-n")
    cmd.Stdout = outWriter
    go tail(outReader)
    cmd.Run()
}

SSE 部分请自行实现 (提示:github上搜一下),使用nginx进行反向代理。

html部分

SmoothieChart
js/widget/performance/main.js

(function ($, SmoothieChart) {
    var chart = new SmoothieChart({
        minValue:0,
        maxValue:100,
        grid:{
            fillStyle:'#FFFFFF',
            strokeStyle:'#CCCCCC',
            sharpLines:true
        },
        labels: {
            fillStyle:'#333333',
        }
    }),
    canvas = document.getElementById('performance-widget'),
    series = new TimeSeries();

    chart.addTimeSeries(series, {lineWidth:2,strokeStyle:'#009A61',fillStyle:'rgba(0,154,97,.1)'});
    chart.streamTo(canvas, 500);

    if (!!window.EventSource) {
        var source = new EventSource('//your/sse/sever/and/path');
        source.addEventListener('performance', function (e) {
            series.append(+new Date(), +e.data);
        }, false);
    }

})(jQuery, SmoothieChart);

PS: Chrome的开发工具暂时无法看到浏览器返回的值,调试的话可以访问:chrome://view-http-cache/http(s)://your/sse/sever/and/path

使用 redis 做限流

背景

事情是这样来的,最近2天 tool.lu 的 uv 并没有大幅的增长,但是 pv 的涨幅却很大,造成服务器的 load 一度超过了 20,想必是被攻击,做过php-fpm的优化,收效甚微。故想到了限流

限流的使用场景

  1. API的调用次数限制
  2. 防止频繁刷新
  3. etc...

想法和实现

其实我是参考了下面这篇文章的实现,至于为什么要这么做,我的考虑跟他文章中提到的是一致的,可以查看原文

Rate limiting with Redis

原文中提供了大段的文字说明,于是我根据他的说明,做了一幅图,仅供参考:

design.jpg

这里需要说明的是,原文中使用的是redis中的hash,但是hash对其中的每个元素没有单独的失效时间,所以使用原文的方法是有bug的;

具体情况请看 issue

这里改用string类型来存储就可以啦!注意将 bucketSpanexpire 设置为同一个值

优化

使用string类型来存储,可以发现key的空间占用比较多,更耗内存了;我们可以压缩一下key来实现节省内存的目的。

实际应用

  1. tool.lu web限流 (php实现版本)
  2. coderunner sandbox限流 (go实现版本)

访问可视化

网站的统计

由于 tool.lu 的流量还不是很大,所以我把每次的访问记录都存到了MySQL(如果流量大,这么做是作死的节奏)

主要流程图

design1.jpg

design2.jpg

使用canal做异步处理,主要是因为

  1. ip => city的映射,可能要调用第三方接口比较耗时
  2. 网站代码处不需要写2份数据
  3. 装x

其中cannal分发的数据处理是用的java,本打算sse也用java的netty来实现了,惭愧,尝试未果后就放弃了,最后用golang实现的。

这样做不会太耗性能,而且每秒钟往客户端传输一次数据,但是由于 vps 的内存有限,java 又比较吃内存,所以上线之后就直接下线了。

youtube-dl无法下载的问题

使用youtube-dl下载youtube视频的时候出现下面的错误

ERROR: content too short

进过一番google之后,说是youtube服务器端那边的问题,可能会修复。最简单的解决办法就是更改一个清晰度差点的(默认下载选择的质量最好的那个),于是:

# 列出所有可用的
youtube-dl [url] -F
# format code extension resolution  note
# 139         m4a       audio only  DASH audio   51k , audio@ 48k (22050Hz), 19.03MiB (worst)
# 140         m4a       audio only  DASH audio  130k , audio@128k (44100Hz), 50.81MiB
# 141         m4a       audio only  DASH audio  258k , audio@256k (44100Hz), 102.01MiB
# 160         mp4       256x144     DASH video  122k , video only, 31.80MiB
# 133         mp4       426x240     DASH video  269k , video only, 50.52MiB
# 134         mp4       640x360     DASH video  281k , video only, 57.12MiB
# 135         mp4       854x480     DASH video  716k , video only, 134.81MiB
# 136         mp4       1280x720    DASH video  930k , video only, 104.83MiB
# 17          3gp       176x144
# 36          3gp       320x240
# 5           flv       400x240
# 43          webm      640x360
# 18          mp4       640x360
# 22          mp4       1280x720    (best)

# 选择一个质量稍差的

youtube-dl [url] -f18

从 php 内核挂载钩子解密源码

背景

大多数的php代码加密(不需要额外扩展就能运行的)原理上都是使用eval进行代码的执行,理论上,只要我们在php内核执行eval函数的时候,将其dump出来,就可以得到源代码。需要注意的是:

  1. 用户上传的代码是不可信的,因此需要一个沙盒
  2. 此法虽然方便,看似是一个万能解密的办法,但是 dump 数据的时候会有很多中间值,还是需要人工的做一个特征库,去识别过滤出需要的代码段

实现

在 php 扩展中, module init 的时候替换掉 zend_compile_string,主要代码如下

static zend_op_array *edump_compile_string(zval *source_string, char *filename TSRMLS_DC)
{
    int c, len;
    char *copy;
 
    if (Z_TYPE_P(source_string) != IS_STRING) {
        return orig_compile_string(source_string, filename TSRMLS_CC);
    }
 
    len  = Z_STRLEN_P(source_string);
    copy = estrndup(Z_STRVAL_P(source_string), len);
    if (len > strlen(copy)) {
        for (c=0; c<len; c++) if (copy[c] == 0) copy[c] == '?';
    }
 
    php_printf("----- [tool.lu start] -----\n");
    php_printf("%s\n", copy);
    php_printf("----- [tool.lu end] -----\n");
 
    yes = 1;

    return orig_compile_string(source_string, filename TSRMLS_CC);
}

PHP_MINIT_FUNCTION(edump)
{
    if (edump_hooked == 0) {
        edump_hooked = 1;
        orig_compile_string = zend_compile_string;
        zend_compile_string = edump_compile_string;
    }
    return SUCCESS;
}

使用docker作为沙盒

使用docker作为沙盒

背景

其实早就想做个在线代码运行的,但是 CentOS6 对 Docker 的支持不是很好,坑比较多。待 CentOS7 出来后思考了一段时间,最终还是决定做起来了。

怎么做

最初考虑的时候就是想着可以扩展性比较好的解决方案,建立一个socket sever可以把文件分发到不同的机器上,再后来便想着还是直接用rsync吧,然后...神马都没弄,就直接本地 mount 到 docker container 里面去了,做了个池子,最多只能开50个docker container

Dockerfile

FROM centos:latest

RUN yum install gcc gcc-c++ php golang -y

ADD entrypoint.sh entrypoint.sh
ADD run-code.sh run-code.sh

ENTRYPOINT ["/bin/bash", "entrypoint.sh"]

至于前端的东西,是用的CodeMirror,给他加了个命令和快捷键(mac 和 pc 区分)

preview

代码在线运行

Screenshot 2014-09-21 at 22.22.18.png

嵌入演示

nginx兼容%23

问题起源

在 tool.lu 上,将分享服务改为百度分享的时候,pc 上的分享链接都正常,但是微信二维码扫描之后就会出现下面的 url,%23也就是一个#

http://tool.lu/news/%2310006-weixin-1-6358-0629b82e8bd20c82f766611c23eca2f9

即使是在其提供的 api 中关闭#回流统计的功能,对二维码来说还是不起作用的。为对此url做兼容,修改 nginx 配置文件如下

解决方案

# hack for baidu share weixin
    rewrite ^(.*)\#(.*)$ $1#$2 redirect;
# end hack¬

使用casperjs截出优雅的图片

前言

  1. phantomjs中文问题
  2. phantomjs中文问题 [后续]

优化字体的显示

使用Chrome OS字体来代替serif, sans-serif, sans, monospace字体

/usr/share/fonts/default/truetype/croscorefonts
├── Arimo-BoldItalic.ttf
├── Arimo-Bold.ttf
├── Arimo-Italic.ttf
├── Arimo-Regular.ttf
├── Cousine-BoldItalic.ttf
├── Cousine-Bold.ttf
├── Cousine-Italic.ttf
├── Cousine-Regular.ttf
├── fonts.dir
├── fonts.scale
├── SymbolNeu.ttf
├── Tinos-BoldItalic.ttf
├── Tinos-Bold.ttf
├── Tinos-Italic.ttf
└── Tinos-Regular.ttf

0 directories, 15 files

~/.fonts.conf

<?xml version='1.0'?>
<!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
<fontconfig>

  <!-- Set preferred serif, sans serif, and monospace fonts. -->
  <alias>
    <family>serif</family>
    <prefer><family>Tinos</family></prefer>
  </alias>
  <alias>
    <family>sans-serif</family>
    <prefer><family>Arimo</family></prefer>
  </alias>
  <alias>
    <family>sans</family>
    <prefer><family>Arimo</family></prefer>
  </alias>
  <alias>
    <family>monospace</family>
    <prefer>
        <family>Meslo LG L DZ</family>
        <family>Cousine</family>
    </prefer>
  </alias>

  <!-- Aliases for commonly used MS fonts. -->
  <match>
    <test name="family"><string>Arial</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Arimo</string>
    </edit>
  </match>
  <match>
    <test name="family"><string>Helvetica</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Arimo</string>
    </edit>
  </match>
  <match>
    <test name="family"><string>Verdana</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Arimo</string>
    </edit>
  </match>
  <match>
    <test name="family"><string>Tahoma</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Arimo</string>
    </edit>
  </match>
  <match>
    <!-- Insert joke here -->
    <test name="family"><string>Comic Sans MS</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Arimo</string>
    </edit>
  </match>
  <match>
    <test name="family"><string>Times New Roman</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Tinos</string>
    </edit>
  </match>
  <match>
    <test name="family"><string>Times</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Tinos</string>
    </edit>
  </match>
  <match>
    <test name="family"><string>Courier New</string></test>
    <edit name="family" mode="assign" binding="strong">
      <string>Cousine</string>
    </edit>
  </match>

  <match target="font" >
    <edit mode="assign" name="autohint" >
      <bool>true</bool>
    </edit>
  </match>
  <match target="font" >
    <edit mode="assign" name="rgba" >
      <const>none</const>
    </edit>
  </match>
  <match target="font" >
    <edit mode="assign" name="hinting" >
      <bool>false</bool>
    </edit>
  </match>
  <match target="font" >
    <edit mode="assign" name="hintstyle" >
      <const>hintnone</const>
    </edit>
  </match>
  <match target="font" >
    <edit mode="assign" name="antialias" >
      <bool>true</bool>
    </edit>
  </match>
</fontconfig>
fc-cache -fv
# 查看一下字体是否选择正确
fc-match monospace

js脚本

example.js

var fs = require('fs');

var casper = require('casper').create({
    stepTimeout: 3000
});

var url = casper.cli.get(0);
url = url || 'http://tool.lu/';

casper.start().viewport(1280, 800).thenOpen(url, function () {
    // this.captureSelector('snap.png', '.post');
    var filename = 'snap.png';
    this.capture(filename);
    this.echo(JSON.stringify({url: this.getCurrentUrl(), path : fs.absolute(filename)}));
});

casper.run();
casperjs example.js http://tool.lu/

效果图

snap.png

java中的线程池

为什么要使用线程池

  1. 重复利用已创建的线程降低线程创建和销毁造成的消耗
  2. 提高线程的可管理性,可进行统一的分配,调优和监控

线程池的处理流程

threadpool.jpg

ExecutorService

newCachedThreadPool();
newFixedThreadPool();
newScheduledThreadPool();
newSingleThreadExecutor();

ScheduledThreadPool使用优先级队列进行排序(距离下次调度间隔短的任务排在前面)
DelayedWorkQueue


package lu.tool.demo;

import java.util.concurrent.*;

/**
 * Created by xiaozi on 14-8-28.
 */
public class CachedThreadPoolDemo {

    public static void main(String[] args) {
        ExecutorService es = Executors.newCachedThreadPool();
        ThreadPoolExecutor tpe = (ThreadPoolExecutor) es;

//        tpe.setMaximumPoolSize(100);

        Future<?> future = null;
//        for (int i = 1; i < 8000; i++) {
        for (int i = 1; i < 100; i++) {
            future = tpe.submit(new TaskDemo());
        }

        System.out.println("largest pool size: " + tpe.getLargestPoolSize());
        System.out.println("task count: " + tpe.getTaskCount());

        if (future != null) {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }

//        tpe.shutdown();
    }
}
package lu.tool.demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * Created by xiaozi on 14-8-28.
 */
public class FixedThreadPoolDemo {

    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(100);
        ThreadPoolExecutor tpe = (ThreadPoolExecutor) es;

        for (int i = 1; i < 8000; i++) {
            tpe.submit(new TaskDemo());
        }

        tpe.shutdown();
    }
}
package lu.tool.demo;

import java.util.concurrent.*;

/**
 * Created by xiaozi on 14-8-28.
 */
public class ScheduledThreadPoolDemo {

    public static void main(String[] args) {
        ScheduledExecutorService es = Executors.newScheduledThreadPool(100);

        for (int i = 1; i < 8000; i++) {
            es.schedule(new TaskDemo(), 3, TimeUnit.SECONDS);
        }

        // scheduleAtFixedRate
        // scheduleWithFixedDelay

        es.shutdown();
    }
}
package lu.tool.demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by xiaozi on 14-8-28.
 */
public class SingleThreadExecutorDemo {

    public static void main(String[] args) {
        ExecutorService es = Executors.newSingleThreadExecutor();

        for (int i = 1; i < 8000; i++) {
            es.submit(new TaskDemo());
        }

        es.shutdown();
    }
}

ThreadPoolExecutor

ThreadPoolExecutor(int corePoolSize,
                    int maximumPoolSize,
                    long keepAliveTime,
                    TimeUnit unit,
                    BlockingQueue<Runnable> workQueue,
                    RejectedExecutionHandler handler);
参数解释
corePoolSize线程池维护线程的最少数量
maximumPoolSize线程池维护线程的最大数量
keepAliveTime线程池维护线程所允许的空闲时间
unit线程池维护线程所允许的空闲时间的单位
workQueue线程池所使用的缓冲队列
handler线程池对拒绝任务的处理策略
package lu.tool.demo;


import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Created by xiaozi on 14-8-29.
 */
public class ThreadPoolExecutorDemo {

    public static void main(String[] args) {
        ThreadPoolExecutor tpe = new ThreadPoolExecutor(3, 5, 60,
                                                        TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10),
                                                        new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 1; i < 8000; i++) {
            tpe.submit(new TaskDemo());
        }

        tpe.shutdown();
    }
}

阻塞队列

ArrayBlockingQueu   // 有界
LinkedBlockingQueue // 无界
SynchronousQueue
PriorityBlockingQueue

线程池的监控

beforeExecute(Thread t, Runnable r)
afterExecute(Runnable r, Throwable t)

executor.getTaskCount();
executor.getCompletedTaskCount();

executor.getLargestPoolSize();

executor.isShutdown();  
executor.isTerminated();

多少线程合适

  1. CPU密集型任务(ncpu + 1)
  2. I/O密集型任务(2 * ncpu)
  3. 任务的执行时间
  4. 任务的依赖性

阻塞队列的实现

线程池队列
ScheduledThreadPoolDelayedWorkQueueReentrantLock.newConditionavailable
FixedThreadPoolLinkedBlockingQueueReentrantLock.newConditionput, take
CachedThreadPoolSynchronousQueueReentrantLock
SingleThreadLinkedBlockingQueueReentrantLock.newConditionput, take

引用

  1. 聊聊并发(三)——JAVA线程池的分析和使用
  2. 线程池
  3. Java四种线程池的使用
  4. 多线程之线程池探索
  5. 深入浅出多线程(4)对CachedThreadPool OutOfMemoryError问题的一些想法
  6. ScheduledThreadPoolExecutor实现原理
  7. JAVA线程池ThreadPoolExecutor
  8. JAVA线程池学习以及队列拒绝策略

phantomjs中文问题 [后续]

之前写过一篇文章:phantomjs中文问题

里面的解决办法也是碰巧,如今将解决问题之所在。

# 列出所有已经安装的中文字体,如果还没有安装中文字体,可参考上文提到的文章。
fc-list :lang=zh
# 宋体,SimSun:style=Regular

# 看下字体是怎么走的
fc-match Arial -s
# SimSun.ttf: "SimSun" "Regular"
# n019003l.pfb: "Nimbus Sans L" "Regular"
# cursor.pfa: "Cursor" "Regular"
# d050000l.pfb: "Dingbats" "Regular"
# s050000l.pfb: "Standard Symbols L" "Regular"
# 哎呀,是对的呀,为什么phantomjs不认呢,(ÒωÓױ)

# 查看下本地语言设置
locale
# LANG=zh_CN.UTF-8
# LC_CTYPE=zh_CN.UTF-8
# LC_NUMERIC="zh_CN.UTF-8"
# LC_TIME="zh_CN.UTF-8"
# LC_COLLATE="zh_CN.UTF-8"
# LC_MONETARY="zh_CN.UTF-8"
# LC_MESSAGES="zh_CN.UTF-8"
# LC_PAPER="zh_CN.UTF-8"
# LC_NAME="zh_CN.UTF-8"
# LC_ADDRESS="zh_CN.UTF-8"
# LC_TELEPHONE="zh_CN.UTF-8"
# LC_MEASUREMENT="zh_CN.UTF-8"
# LC_IDENTIFICATION="zh_CN.UTF-8"
# LC_ALL=


# 嗯,据说 QT 在对“宋体,SimSun:style=Regular”处理的时候是有问题的
export LC_CTYPE=en_US.UTF-8
fc-list :lang=zh
# SimSun,宋体:style=Regular
# 换了个顺序,试试...,呵呵,phantomjs中文的问题解决了 - - |||

因为不需要全局修改LC_CTYPE,所以在命令前面加一下就好了。为了方便写个alias

alias casperjs='LC_CTYPE=en_US.UTF-8 casperjs'

一些php扩展的资料整理

Thread-Safe 宏

TSRM

Thread-Safe Resource Manager
线程安全资源管理

ZTS

Zend Thread Safety
Zend 线程安全

tsrm_ls

TSRM local storage
线程安全资源管理 本地存储

TSRMLS_??

TSRMLS_C tsrm_ls
TSRMLS_D void ***tsrm_ls
TSRMLS_CC , tsrm_ls
TSRMLS_DC , void ***tsrm_ls
CC - call with comma
     前置逗号的调用
C  - call
     调用
DC - declaration with comma
     前置逗号的声明
D  - declaration
     声明

zend_parse_parameters

类型符号

类型字符php参数类型c类型
llonglong
ddoubledouble
sstringchar* / int
bbooleanzend_bool
rresourcezval*
aarrayzval*
zzvalzval*
Zzvalzval**
ffunctionfunction
o/Oclasszval, zend_class_entry

控制字符

控制字符作用
|它之前的参数都是必须的,之后的都是非必须的,也就是有默认值的。
!如果接收了一个PHP语言里的null变量,则直接把其转成C语言里的NULL,而不是封装成IS_NULL类型的zval。
/如果传递过来的变量与别的变量共用一个zval,而且不是引用,则进行强制分离,新的zval的is_ref__gc==0, and refcount__gc==1.

返回值

返回函数
RETURN_RESOURCE(resource)
RETURN_BOOL(bool)
RETURN_NULL()
RETURN_LONG(long)
RETURN_DOUBLE(double)
RETURN_STRING(string, duplicate)
RETURN_STRINGL(string, length, duplicate)
RETURN_EMPTY_STRING()
RETURN_FALSE()
RETURN_TRUE()

helpers

helperexpanded解释
ZEND_FE_END(){NULL, NULL, NULL}用于zend_function_entry的最后一行
ZEND_STRL("str")"str", strlen("str")用于同时需要字符串和其长度的函数中

Bash杂记

取出文件列表

# 防止WordSplitting,避免使用$(ls *.txt)
for file in *.txt
do
    # 防止文件名以-开头
    cp "./$file" /path/to/target
done

按行读取文件

while read line
do
    echo "$line"
done < text.txt

拷贝文件

cp -- "$file" "$target"
# -- 防止文件名以-开头
# " 防止文件名中含有空格

字符串比较

[[ $foo == "$bar" ]]

cd到目录

# -P prefix
cd -P -- "$(dirname -- "$f")"

数字比较

((foo > 7))
# 或者
[ "$foo" -gt 7 ]

判断文件中是否包含某个字符串

if grep -q fooregex /path/to/file; then
    # do something here
fi

多条件判断

if [ a = b ] && [ c = d ]
# 或者
if [[ a = b && c = d ]]

对文件的修改

先修改到临时文件,再mv回去

多行字符串

# 不要使用echo
cat <<EOF
  Hello world
  How's it going?
EOF

对cd命令是否成功的判断

cd /foo && bar
# 或者有很多依赖于cd之后的命令
cd /foo || exit 1
# ...
# ...
# 或者顺便说点什么 
cd /foo || { echo "hi, man!"; exit 1; }

for循环

for ((i=1; i<=n; i++)); do
    # do something here
done

错误重定向

# 先重定向到文件,再定向到标准输出(标准输出已经到tty了)
somecmd >>logfile 2>&1