docker 清理 /var/lib/docker/aufs 目录

内网服务器上一直用 Docker 来 build 镜像,来进行持续发布,使用一段时间之后,会生成很多文件,如果不及时清理,会占据大量的 inode,虽然磁盘没满,但是无法分配 inode,也会导致一些命令不可用。

查看 inode 数量命令

df -i

如何锁定目录

如果是 / 根目录占满了,怎么确认是哪个子目录呢?使用这条命令依次深入目录执行,查看计算结果,最终就会确认下是哪个目录。

for i in *; do echo $i; find $i | wc -l; done

最后发现是 /var/lib/docker/aufs/diff 这个目录,直接删除 aufs 目录下所有文件,删除 base 镜像,然后重启 docker 服务,清理结束。

cd /var/lib/docker/aufs
rm -r *
docker rmi [你的基础镜像 ID]
service docker restart

不用担心,重启之后会自动重建 diff 目录。
再次执行 df -i 的时候,inode 的百分比已经降下来了。

附加一些批量操作命令:

批量删除镜像

docker images | awk '{print $3}' | xargs docker rmi

批量删除容器

docker ps -a | awk '{print $1}' | xargs docker rm

参考链接:
https://segmentfault.com/q/1010000003106521

记一次 Laravel 项目迁移之后 Model 报错问题

  之前迁移过一个 Laravel 5.3 的网站,发布完代码,composer update 之后,能正常访问,随便点了点就再没去管它,后来在后台点击反馈模块就报错,当时在 laravel.log 看到 sql 语句是表名后面没有 s,那肯定报错啊,于是徒手在那个 Model 里面指定上 $table,解决了之后,也就没去深究,后来感觉心里越来越不安,虽然不是我写的,但没去深究,就感觉有罪恶感,于是决定重现这个问题来深入研究一下。

问题现象

数据库有数据表 feedbacks, 对应的 Model 为 Feedback.php 内部没有指定 $table.
在我本地是没有问题的,可以正确指向到 feedbacks 表,于是我从服务器上把代码打了个包,download 到本地重放,果然在本地也报错,可以断言是代码的问题。

代码是这个样子

Feedback.php
<?php 

namespace App\Http\Models;

use Illuminate\Database\Eloquent\Model;

class Feedback extends Model {

    protected $fillable = [];

    protected $dates = [];

    public static $rules = [

    ];

}

报错是这个样子

[2018-03-28 19:59:40] production.ERROR: PDOException: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'xxx.feedback' doesn't exist in /www/....../vendor/laravel/framework/src/Illuminate/Database/Connection.php:333

开始排查代码

Step 1. 打印表名

在调用 Feedback 模型之前打印表名出来看看,结果是 feedback 没有 s,报错是肯定的!

$feedbackObj = new Feedback();
$table = $feedbackObj->getTable();
dump( $table );

Step 2. 进入 Model.php 排查

文件路径:/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php, 跳转到 getTable 方法 从源码很容易看出,如果我在模型里面指定了 $table,会走 if 这块代码直接返回自己设置的表名,如果我没有设置 table,肯定走的下面的自动获取表名逻辑,既然锁定了问题出在自动获取表名这里,就在 return 之前依次打印结果观察。

源码如下:

    /**
     * Get the table associated with the model.
     *
     * @return string
     */
    public function getTable()
    {
        if (isset($this->table)) {
            return $this->table;
        }

        return str_replace('\\', '', Str::snake(Str::plural(class_basename($this))));
    }

打印代码及打印结果如下:

dump( class_basename($this) );    // Feedback
dump( Str::plural( class_basename($this) ) );    // Feedback
dump( Str::snake( Str::plural( class_basename($this) ) ) );    // feedback

从打印结果来看 Str::plural( class_basename($this) ) 这一行已经出现问题了

Step 3. 继续进入 Str.php 排查

文件路径:/vendor/laravel/framework/src/Illuminate/Support/Str.php, 跳转到 plural 方法

代码很简单,获取英文单词的复数形式,用 Pluralizer 类去调用 plural 静态方法

    /**
     * Get the plural form of an English word.
     *
     * @param  string  $value
     * @param  int     $count
     * @return string
     */
    public static function plural($value, $count = 2)
    {
        return Pluralizer::plural($value, $count);
    }

Step 4. 继续进入 Pluralizer.php 排查

文件路径:/vendor/laravel/framework/src/Illuminate/Support/Pluralizer.php, 跳转到 plural 方法

if 这段代码不会走,因为 $count 默认是2,feedback 这个单词没有在 $uncountable 这个数组里面出现,两个条件没有一个成立的。
继续打印 $plural,打印结果 Feedback,这里就有问题了。

源码如下:

    /**
     * Get the plural form of an English word.
     *
     * @param  string  $value
     * @param  int     $count
     * @return string
     */
    public static function plural($value, $count = 2)
    {
        if ((int) $count === 1 || static::uncountable($value)) {
            return $value;
        }

        $plural = Inflector::pluralize($value);

        return static::matchCase($plural, $value);
    }

Step 5. 继续进入 Inflector.php 排查

文件路径:/vendor/doctrine/inflector/lib/Doctrine/Common/Inflector/Inflector.php, 跳转到 pluralize 方法。

    /**
     * Returns a word in plural form.
     *
     * @param string $word The word in singular form.
     *
     * @return string The word in plural form.
     */
    public static function pluralize(string $word) : string
    {
        if (isset(self::$cache['pluralize'][$word])) {
            return self::$cache['pluralize'][$word];
        }

        if (!isset(self::$plural['merged']['irregular'])) {
            self::$plural['merged']['irregular'] = self::$plural['irregular'];
        }

        if (!isset(self::$plural['merged']['uninflected'])) {
            self::$plural['merged']['uninflected'] = array_merge(self::$plural['uninflected'], self::$uninflected);
        }

        if (!isset(self::$plural['cacheUninflected']) || !isset(self::$plural['cacheIrregular'])) {
            self::$plural['cacheUninflected'] = '(?:' . implode('|', self::$plural['merged']['uninflected']) . ')';
            self::$plural['cacheIrregular']   = '(?:' . implode('|', array_keys(self::$plural['merged']['irregular'])) . ')';
        }

        if (preg_match('/(.*)\\b(' . self::$plural['cacheIrregular'] . ')$/i', $word, $regs)) {
            self::$cache['pluralize'][$word] = $regs[1] . $word[0] . substr(self::$plural['merged']['irregular'][strtolower($regs[2])], 1);

            return self::$cache['pluralize'][$word];
        }

        if (preg_match('/^(' . self::$plural['cacheUninflected'] . ')$/i', $word, $regs)) {
            self::$cache['pluralize'][$word] = $word;

            return $word;
        }

        foreach (self::$plural['rules'] as $rule => $replacement) {
            if (preg_match($rule, $word)) {
                self::$cache['pluralize'][$word] = preg_replace($rule, $replacement, $word);

                return self::$cache['pluralize'][$word];
            }
        }
    }

这个 function 里面 if 判断很多,通过打印锁定在这一行

if (preg_match('/^(' . self::$plural['cacheUninflected'] . ')$/i', $word, $regs))

self::$plural['cacheUninflected'] 打印结果里面发现了 feedback 这个单词,原因就是在这里了,但这个单词是怎么来的呢?

"(?:.*[nrlm]ese|.*deer|.*fish|.*measles|.*ois|.*pox|.*sheep|people|cookie|police|.*?media|Amoyese|audio|bison|Borghese|bream|breeches|britches|buffalo|cantus|carp|chassis|clippers|cod|coitus|compensation|Congoese|contretemps|coreopsis|corps|data|debris|deer|diabetes|djinn|education|eland|elk|emoji|equipment|evidence|Faroese|feedback|fish|flounder|Foochowese|Furniture|furniture|gallows|Genevese|Genoese|Gilbertese|gold|headquarters|herpes|hijinks|Hottentotese|information|innings|jackanapes|jedi|Kiplingese|knowledge|Kongoese|love|Lucchese|Luggage|mackerel|Maltese|metadata|mews|moose|mumps|Nankingese|news|nexus|Niasese|nutrition|offspring|Pekingese|Piedmontese|pincers|Pistoiese|plankton|pliers|pokemon|police|Portuguese|proceedings|rabies|rain|rhinoceros|rice|salmon|Sarawakese|scissors|sea[- ]bass|series|Shavese|shears|sheep|siemens|species|staff|swine|traffic|trousers|trout|tuna|us|Vermontese|Wenchowese|wheat|whiting|wildebeest|Yengeese)"

顺着往上找发现在 第三个 if 判断的时候执行了这一行代码,self::$uninflected 这个是关键,马上查找这个变量。

if (!isset(self::$plural['merged']['uninflected'])) {
    self::$plural['merged']['uninflected'] = array_merge(self::$plural['uninflected'], self::$uninflected);
}

在 :223 行找到了这个变量的所有值,这个变量的意思是复数是单词原形,不受影响,What?feedback 复数不加 s?顺手查了一下,百度词典,金山词霸很明确的说复数加s,有道词典没有说明,只显示 feedbacks 是名词回馈的意思,通过查了一些资料还是推荐 feedback 为复数形式。
参考资料链接:
http://www.learnenglishwithwill.com/feedback-vs-feedbacks-plural-form/

    /**
     * Words that should not be inflected.
     *
     * @var array
     */
    private static $uninflected = array(
        '.*?media', 'Amoyese', 'audio', 'bison', 'Borghese', 'bream', 'breeches',
        'britches', 'buffalo', 'cantus', 'carp', 'chassis', 'clippers', 'cod', 'coitus', 'compensation', 'Congoese',
        'contretemps', 'coreopsis', 'corps', 'data', 'debris', 'deer', 'diabetes', 'djinn', 'education', 'eland',
        'elk', 'emoji', 'equipment', 'evidence', 'Faroese', 'feedback', 'fish', 'flounder', 'Foochowese',
        'Furniture', 'furniture', 'gallows', 'Genevese', 'Genoese', 'Gilbertese', 'gold', 
        'headquarters', 'herpes', 'hijinks', 'Hottentotese', 'information', 'innings', 'jackanapes', 'jedi',
        'Kiplingese', 'knowledge', 'Kongoese', 'love', 'Lucchese', 'Luggage', 'mackerel', 'Maltese', 'metadata',
        'mews', 'moose', 'mumps', 'Nankingese', 'news', 'nexus', 'Niasese', 'nutrition', 'offspring',
        'Pekingese', 'Piedmontese', 'pincers', 'Pistoiese', 'plankton', 'pliers', 'pokemon', 'police', 'Portuguese',
        'proceedings', 'rabies', 'rain', 'rhinoceros', 'rice', 'salmon', 'Sarawakese', 'scissors', 'sea[- ]bass',
        'series', 'Shavese', 'shears', 'sheep', 'siemens', 'species', 'staff', 'swine', 'traffic',
        'trousers', 'trout', 'tuna', 'us', 'Vermontese', 'Wenchowese', 'wheat', 'whiting', 'wildebeest', 'Yengeese'
    );

代码找到这里,这个问题就已经明白了,是因为 update 了 doctrine/inflector 这个包导致的。

Step 6. 继续深究

于是重开一个目录,pull 一下这几个版本发现 1.3.0 开始发生了变化,加入了 feedback 没有复数形式。

composer require doctrine/inflector 1.2.0
composer require doctrine/inflector 1.3.0

继续开新目录 Clone 源代码分析:

git clone https://github.com/doctrine/inflector.git 

查看 git log 看到了这个注释信息 Added more uninflected words

探究到这里,我想这个问题真的明白了。

按照惯例得总结一下结尾:

  1. Model 里面尽量指定一个 $table,有可能把握不准单词复数的形式。
  2. composer update 之后要通过 composer.lock 检查有版本变化的包。
  3. 英文真的很重要。
  4. 源码面前,了无秘密。
  5. 祝阅读到最后的人技术再上一个 level。

php 使用 GeoIP 扩展获取 ip 各种信息

项目背景


公司 App 上准备针对客户的ip来推荐最优服务器来快连设备,目前有4个节点,北京、俄勒冈、新加坡、法兰克福。客户端上报 IP,云端根据分配规则返回 server 代号。

资料搜集


一开始打算找第三方的一些API,测试了很多ip定位接口,效果并不好,有的收费,有的需要申请key有调用次数限制,大部分API只有国家和城市的信息,而且不规范,不是国家代码,抓取到结果还需要进一步匹配,因为没有大洲信息,还要自己根据国家去 mapping 大洲。

上面的各种缺点,直接放弃第三方的API,于是找到了 GeoIP 这个扩展,这个有纯PHP版本的,但是我没找到如何获取大洲,直接上C扩展版的,性能肯定没问题,装好扩展,geoip_continent_code_by_name 直接获取大洲简称代码。

php 扩展安装


我的是 Docker 环境 Ubuntu14.04 php5.6, 下面是扩展的安装命令。

apt-get -y --force-yes install php5.6-geoip

注意:--no-install-recommends 这个参数一定不要加,有这个的话安装完成会把 IP 数据包删除的, 我 build docker 镜像的时候吃过亏了,下面是 IP 数据包的目录。

root@1e1931746c6c:/usr/share/GeoIP# ls
GeoIP.dat  GeoIPv6.dat

GeoIP 扩展源码下载: https://pecl.php.net/package/geoip
以下是源码安装步骤:

$ wget https://pecl.php.net/get/geoip-1.1.1.tgz
$ cd geoip-1.1.1
$ phpize
$ ./configure
$ make
$ sudo make install

编写代码


在 phpinfo 能看到 GeoIP,就说明扩展安装好了。
这是 GeoIP 扩展的文档,一个函数获取想要的信息,完全满足了我的需求,比第三方 API 好用的多,直接省去网络请求。
http://php.net/manual/zh/book.geoip.php

以 docker 方式安装 Jenkins 的笔记

  准备工作:docker 环境要装好,docker version 能显示出版本信息。
  Docker 安装: https://www.w3cschool.cn/docker/ubuntu-docker-install.html

  1. 首先将镜像 pull 到本地,pull 的过程很慢,有可能卡死,我重试了好几次,成功之后本地大小 809 MB。

    sudo docker pull jenkins
  2. 给 Jenkins 创建一个工作目录,目录名称随意,下面会将这个目录映射到容器。

    mkdir /var/jenkins_home
  3. 为工作目录设置归属用户Id

    sudo chown -R 1000:1000 /var/jenkins_home

    为什么是 1000,可以看看 Dockerfile 就明白了。 https://hub.docker.com/_/jenkins/

  4. 运行容器,8081 可以替换成你自己的端口,8080 不要改动,这是 Jenkins 的容器端口号。

    docker run --name myjenkins -p 8081:8080 -p 50000:50000 -v /var/jenkins_home:/var/jenkins_home -d jenkins
  5. 输入 ip 加 端口号就可以访问 Jenkins 了
    第一次访问需要解锁密码:
    cat /var/jenkins_home/secrets/initialAdminPassword

    卡白屏或一直显示请稍后问题:

    vim /var/jenkins_home/updates/default.json
    替换为百度的网址  "connectionCheckUrl":"http://www.baidu.com/"

    被墙了,所以一直卡住。

接下来安装好推荐插件,设置一个管理员账号,就可以用了。

附一张安装好的截图:

Linux 多条命令同时执行的一些姿势

命令之间用 ; 隔开

说明:各命令的执行结果,不会影响其它命令的执行。换句话说,各个命令都会执行,但不保证每个命令都执行成功。

root@ubuntu:/# echo $HOME; echo $LANG
/root
en_US.UTF-8

命令之间用 && 隔开

说明:若前面的命令执行成功,才会去执行后面的命令。这样可以保证所有的命令执行完毕后,执行过程都是成功的。

root@ubuntu:~# cd Cloud && mkdir test
root@ubuntu:~/Cloud# ls
test
root@ubuntu:~/Cloud# cd Cloudaaa && mkdir test
-bash: cd: Cloudaaa: No such file or directory

命令之间用 || 隔开

说明:|| 是或的意思,只有前面的命令执行失败后才去执行下一条命令

/* && 不会输出 123 */
root@ubuntu:~/Cloud# cd Cloudaaa && echo '123'
-bash: cd: Cloudaaa: No such file or directory
/* 用 || 可以输出 123,因为 cd Cloudaaa 失败了 */
root@ubuntu:~/Cloud# cd Cloudaaa || echo '123'
-bash: cd: Cloudaaa: No such file or directory
123

命令之间用 | 隔开

说明:在命令行中,| 表示管道,管道可以将一个命令的输出导向另一个命令的输入,从而让两个(或者更多命令)像流水线一样连续工作,不断地处理文本流。

root@ubuntu:~/Cloud# env | grep LANG
LANG=en_US.UTF-8
LANGUAGE=en_US:

env 获取所有环境变量将结果传给下一个命令,所以用 grep 搜索 LANG 就会有结果显示。