MySQL 数据类型

了解并熟悉 MySQL 中的数据类型,对建表和数据库优化都非常重要。 MySQL 实现了 SQL 定义的类型,也响应的增加乐意 tiny, small, big 的类型。 MySQL 的数据类型主要分成三个部分:

  • Numeric Type 数值型
  • Date and Time Type 日期和时间
  • String Type 字符型

更多的内容可以在官网 查到。

整型

MySQL 数据类型 大小 范围(有符号)
TINYINT(m) 1 字节 范围 (-128~127)
SMALLINT(m) 2 个字节  范围 (-32768~32767)
MEDIUMINT(m) 3 个字节 范围 (-8388608~8388607)
INT(m) 4 个字节 范围 2^31-1(-2147483648~2147483647)
BIGINT(m) 8 个字节  范围 2^63-1(+-9.22*10 的 18 次方)

取值范围如果加了 unsigned (无符号),则最大值翻倍,如 TINYINT unsigned 的取值范围为 (0~256)。 INT(m) 里的 m 是表示 SELECT 查询结果集中的显示宽度,并不影响实际的取值范围,没有影响到显示的宽度,不知道这个 m 有什么用。

浮点型 (float 和 double)

MySQL 数据类型 大小 含义
float(m,d) 4 字节 单精度浮点型    8 位精度 (4 字节)        m 总个数,d 小数位
double(m,d) 8 字节 双精度浮点型    16 位精度 (8 字节)       m 总个数,d 小数位

设一个字段定义为 float(5,3),如果插入一个数 123.45678, 实际数据库里存的是 123.457,但总个数还以实际为准,即 6 位。

定点数

浮点型在数据库中存放的是近似值,而定点类型在数据库中存放的是精确值。 decimal(m,d) 参数 m<65 是总个数,d<30d<m 是小数位。

字符串 (char,varchar,text)

MySQL 数据类型 大小 含义
char(n) 0-255 字节 固定长度,最多 255 个字符
varchar(n) 0-65535 字节 固定长度,最多 65535 个字符
tinytext 0-255 字节 可变长度,最多 255 个字符
text 0-65535 字节 可变长度,最多 65535 个字符
mediumtext 0-16777 215 字节 可变长度,最多 2 的 24 次方 -1 个字符
longtext 0-4294967295 字节 可变长度,最多 2 的 32 次方 -1 个字符

char 和 varchar:

  1. char(n) 若存入字符数小于 n,则以空格补于其后,查询之时再将空格去掉。所以 char 类型存储的字符串末尾不能有空格,varchar 不限于此。
  2. char(n) 固定长度,char(4) 不管是存入几个字符,都将占用 4 个字节,varchar 是存入的实际字符数 +1 个字节(n<=255)或 2 个字节 (n>255),所以 varchar(4), 存入 3 个字符将占用 4 个字节。
  3. char 类型的字符串检索速度要比 varchar 类型的快。

varchar 和 text:

  1. varchar 可指定 n,text 不能指定,内部存储 varchar 是存入的实际字符数 +1 个字节(n<=255)或 2 个字节 (n>255),text 是实际字符数 +2 个字节。
  2. text 类型不能有默认值。
  3. varchar 可直接创建索引,text 创建索引要指定前多少个字符。varchar 查询速度快于 text, 在都创建索引的情况下,text 的索引似乎不起作用。

char(n)和 varchar(n)中括号中 n 代表字符的个数,并不代表字节个数,所以当使用了中文的时候 (UTF8) 意味着可以插入 m 个中文,但是实际会占用 m*3 个字节。同时 char 和 varchar 最大的区别就在于 char 不管实际 value 都会占用 n 个字符的空间,而 varchar 只会占用实际字符应该占用的空间 +1,并且实际空间 +1<=n

  • 超过 char 和 varchar 的 n 设置后,字符串会被截断
  • char 的上限为 255 字节,varchar 的上限 65535 字节,text 的上限为 65535
  • char 在存储的时候会截断尾部的空格,varchar 和 text 不会
  • varchar 会使用 1-3 个字节来存储长度,text 不会
Value CHAR(4) Storage Required VARCHAR(4) Storage Required
’’ ’ ‘ 4 bytes ’’ 1 byte
‘ab’ ‘ab ‘ 4 bytes ‘ab’ 3 bytes
‘abcd’ ‘abcd’ 4 bytes ‘abcd’ 5 bytes
‘abcdefg’ ‘abcd’ 4 bytes ‘abcd’ 5 bytes

在使用 MySQL 存储字符串时经常会疑惑选择哪一种数据类型。

首先从空间方面,当 varchar 大于某些值时,会自动转换我 text,大概为

  • 大于 varchar(255)变为 tinytext
  • 大于 varchar(500)变为 text
  • 大于 varchar(20000)变为 mediumtext

所以对于大内容 varchar 和 text 并没有太多区别。

其次从性能方面,索引是影响性能最关键的因素,对于 text 来说,只能添加前缀索引,并且索引最大只能 1000 字节。而 varchar 即使超过 1000 字节长,也会被截断。

二进制数据 (Blob)

  1. BLOB 和 TEXT 存储方式不同,TEXT 以文本方式存储,英文存储区分大小写,而 Blob 是以二进制方式存储,不分大小写。
  2. BLOB 存储的数据只能整体读出。
  3. TEXT 可以指定字符集,BLOB 不用指定字符集。
数据类型 大小 用途
TINYBLOB 0~255 字节 不超过 255 个字符二进制字符串
BLOB 0~65535 字节 二进制
MEDIUMBLOB 0-16 777 215 字节 二进制形式的中等长度文本数据
LONGBLOB 0-4 294 967 295 字节 二进制形式的极大文本数据

日期和时间类型

MySQL 数据类型 大小 范围 含义
date 3 字节 1000-01-01/9999-12-31 日期 ‘2008-12-2’
time 3 字节 ‘-838:59:59’/’838:59:59’ 时间 ‘12:25:36’
datetime 8 字节 1000-01-01 00:00:00/9999-12-31 23:59:59 日期时间 ‘2008-12-2 22:06:44’
timestamp 4 字节 1970-01-01 00:00:00/2037 年某时 自动存储记录修改时间

每个时间类型有一个有效值范围和一个”零”值,当指定不合法的 MySQL 不能表示的值时使用”零”值。若定义一个字段为 timestamp,这个字段里的时间数据会随其他字段修改的时候自动刷新,所以这个数据类型的字段可以存放这条记录最后被修改的时间。

Datetime 和 timestamp 的区别

区别 datetime timestamp
空间 8 字节 4 字节
是否允许空值 允许 允许
是否可以自定义值 可以 不可以
支持时间范围 1000-01-01 00:00:00/9999-12-31 23:59:59 不能早于 1970 或者晚于 2037 年
是否与时区相关 无关 值以 UTC 格式保存,存储 milliseconds,需要存储或者取出时手动转换时区
默认值 可以在指定 datetime 字段的值的时候使用 now() 变量来自动插入系统的当前时间 默认值为 CURRENT_TIMESTAMP() ,当前系统时间
结论 dateim 类型适合用来记录数据的原始创建时间,无论如何更改记录中其他字段, datetime 都不会改变,除非手动改变 数据库会自动修改其值,任何修改记录都会被更新,如果需要不设置自动更新,通过设置 DEFAULT CURRENT_TIMESTAMP 可实现。timestamp 类型适合用来记录数据最后修改时间。

数据类型的属性

MySQL 关键字 含义
NULL 数据列可包含 NULL 值
NOT NULL 数据列不允许包含 NULL 值
DEFAULT 默认值
PRIMARY KEY 主键
AUTO_INCREMENT 自动递增,适用于整数类型
UNSIGNED 无符号
CHARACTER SET name 指定一个字符集

2017-07-07 linux , mysql , sql

Java 查漏补缺之枚举

当创建 enum 时,编译器会自动创建一个继承自 java.lang.Enum 的类。

ordinal 方法

ordinal() 方法会返回一个 int 值,是每个 enum 实例声明时的次序,从 0 开始。枚举可以使用 ==比较,编译器会自动提供 equals()hashCode() 方法。 Enum 类实现了 Comparable 接口,具有 compareTo() 方法,同时也实现了 Serializable 接口。

values 方法

values() 方法是由编译器添加的 static 方法。

继承

enum 都继承自 java.lang.Enum ,并且 Java 不支持多继承,所以 enum 不能再继承其他类。然而我们在创建 enum 时可以实现一个或者多个 interface。

随机选择枚举

构造如下的工具类

public class EnumUtils {
    public static <T extends Enum<T>> T random(Class<T> c) {
        return random(c.getEnumConstants());
    }

    private static <T extends Enum<T>> T random(T[] values) {
        return values[new Random().nextInt(values.length)];
    }
}

然后使用

    Fruit random = EnumUtils.random(Fruit.class);
    print(random);

EnumSet

EnumSet 是一种特殊的 Set,这个集合中只能存储 enum 的值。EnumSet 的设计充分考了速度,内部实现就是将一个 long 值作为 bit 向量,所以 EnumSet 非常快。 EnumSet 的基础是 long,一个 long 值有 64 位,一个 enum 实例只需要一位 bit 表示是否存在,也就是说,在不超过 long 的表达能力的情况下, EnumSet 可以应用于最多不超过 64 个元素的 enum。如果超过 64 个元素呢?

当枚举数量小于 64 的时候,创建一个 RegularEnumSet 实例对象,大于 64 时则创建一个 JumboEnumSet 实例对象。枚举项的排序值 ordinal 是从 0,1,2,…… 依次递增的,没有重号,没有跳号,RegularEnumSet 就是利用这一点把每个枚举项的 ordinal 映射到一个 long 类型的每个位上的,

class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
    private long elements = 0L;// 记录所有枚举排序号,注意是 long 型
    RegularEnumSet(Class<E>elementType, Enum[] universe) {// 构造函数
        super(elementType, universe);
    }
    void addAll() {// 加入所有元素
        if (universe.length != 0)
            elements = -1L >>> -universe.length;
    }
}

long 类型是 64 位的,所以 RegularEnumSet 类型也就只能负责枚举项数量,不大于 64 的枚举,大于 64 则由 JumboEnumSet 处理

class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {
    private long elements[];// 映射所有的枚举项
    JumboEnumSet(Class<E>elementType, Enum[] universe) {// 构造函数
        super(elementType, universe);
        elements = new long[(universe.length + 63) >>> 6];// 默认长度是枚举项数量除以 64 再加 1
    }
    void addAll() {//elements 中每个元素表示 64 个枚举项
        for (int i = 0; i < elements.length; i++)
            elements[i] = -1;
        elements[elements.length - 1] >>>= -universe.length;
        size = universe.length;
    }
}

JumboEnumSet 类把枚举项按照 64 个元素一组拆分了多组,每组都映射到一个 long 类型的数字上,然后该数组再放置到 elements 数组中,简单来说 JumboEnumSet 类的原理与 RegularEnumset 相似,只是 JumboEnumSet 使用了 long 数组能容纳更多的枚举项。

EnumMap

EnumMap 是一种特殊的 Map,要求其 key 必须来自一个 enum 。因为 enum 本身的限制,EnumMap 在内部使用数组实现,非常快。和 EnumSet 一样,enum 实例定义的次序决定了在 EnumMap 中的顺序。

enum 每个实例作为键总是存在的,如果没有为这个键调用 put 方法来存入相应的值,对应的值就是 null。

reference

  • Java 编程思想

2017-07-02 java , enum , linux

Java 查漏补缺之并发编程

Java 实现多线程,继承 Thread 类,另一种就是实现 Runnable 接口。实际 Thread 类源码也是实现了 Runnable,使用继承 Thread 方式创建多线程,最大的局限就是不能多继承,随意推荐实现 Runnable 。

Executor

Java 5 开始, Java 并发 API 提供了一套执行器框架 Executor Framework,围绕 Executor 接口和它的子接口 ExecutorService ,以及实现这两个接口的 ThreadPoolExecutor 类展开。这套机制将任务创建和执行分离。执行器通过创建所需的线程来负责 Runnable 对象的创建、实例化和运行。执行器使用线程池来提高应用性能。

执行器另一个优势是 Callable 接口,类似于 Runnable 接口,但是 Callable 接口的 call() 方法能够返回接口;当发送 Callable 对象给 Executor 时,将获得一个实现了 Future 接口的对象,通过这个对象,可以用来控制 Callable 的状态和结果。

Executor 是一个接口,用来表示一个对象能够接受 task 来执行。

执行器需要显示的结束它,否则程序不会结束。执行器没有任何任务可以执行,那么会一直等待。

ExecutorService

ExecutorService 接口继承 Executor 接口,提供做了更多管理生命周期的方法,他提供了内存队列,并且可以通过当前线程的可用性来安排任务执行。

public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit);
    <T> Future<T> submit(Callable<T> task);
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);
    <T> T invokeAny(Collection<? extends Callable<T>> tasks);
}

几个重要的方法:

  • submit 方法调用 Executor.execute() 然后返回 Future
  • invokeAnyinvokeAll 方法来提交一个集合任务,然后等待
  • shutdown 方法平稳关闭,不再接受新任务,同时等待已经提交的任务执行完毕,包括还未开始的任务
  • shutdownNow 方法将直接关闭过程,将尝试取消所有运行中的任务,不再启动队列中尚未开始的任务

ExecutorService 的生命周期有三种:运行,关闭和已终止。

  • ExecutorService 创建初期处于运行状态
  • 当所有任务都完成就进入终止状态

Executors

Executors 是一个工具类。 Executors 类中有很多创建线程池的方法,这些方法都是调用

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) { ... }

参数说明

  • corePoolSize 基本大小,在没有任务执行时,线程池的大小
  • maximumPoolSize 最大大小,可同时活动的线程数量上限
  • keepAliveTime 当某个线程空闲时间超过存活时间,会被标记为回收,当线程池大小超过基本大小时,该线程会被终止

在 Executors 中提供了很多静态方法:

  • newFixedThreadPool(int) 固定长度线程池
  • newCachedThreadPool() 可缓存线程池,线程池规模不存在任何限制
  • newSingleThreadExecutor() 单线程 Executor,如果这个线程出现异常,将创建新的线程补充。能够确保任务在队列中顺序执行,FIFO
  • newScheduledThreadPool(int) 固定长度线程池,延迟或定时执行任务,类似 Timer

ScheduledExecutorService

ScheduledExecutorService 和 ExecutorService 接口类似,但是提供了定时任务的方法。

Future

Future 用来表示异步操作的结果。他有方法可以用来检测任务有没有完成,也有方法来获取异步任务的结果。

volatile

关键字 volatile 主要作用是让变量在多个线程间可见。

volatile vs synchronized

  • volatile 关键字是程序同步轻量级实现,性能稍好,volatile 只能修饰变量,而 synchronized 可以修饰方法,代码块
  • 多线程访问 volatile 不会阻塞, synchronized 会阻塞
  • volatile 能保证数据可见性,不能保证原子性;synchronized 可以保证原子性,也能间接保证可见性,synchronized 会将私有内存和公共内存的数据同步。

thread setDaemon(boolean)

关于 Thread 类中 setDaemon(boolean) 中的 daemon 方法,一个守护线程是程序运行结束仍然运行的线程,垃圾回收线程就是典型的例子。在 Java 中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 。只要当前 JVM 实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着 JVM 一同结束工作。

User 和 Daemon 两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread 已经全部退出运行了,只剩下 Daemon Thread 存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon 也就没有工作可做了,也就没有继续运行程序的必要了。


2017-06-27 java , thread , runnable , callable , executor

Java 查漏补缺之 defensive copy

什么是保护性拷贝(defensive copy,或者防御性拷贝)呢? 说到这个问题首先要知道 Java 中的不可变对象(Immutable Object),既然有不可变对象就一定有可变对象(Mutable Object),顾名思义可变对象就是在对象构造完成之后内部状态会改变的对象,比如 StringBuilder 和 Date 都是可变对象,而 String 和 Integer 是不可变对象。

一个类可能有两种方式包含可变的 field

  • 类状态只能被自身改变,也就是说类包含一个可变对象,而该字段只能由类自己改变,比如说类 Person 有一个字段 birthDate,而该字段由 Person 类在创建时创建
  • 另外一种类的状态可以被自身和调用者改变,也就是说类对象中包含在其他地方创建的可变对象,比如下面的例子中,假如构造函数中的 Date 没有进行保护性拷贝,可能导致错误

如果在构建类时只想要类状态被自身改变,那么在可变对象被传入或者被外部获取时,保护性拷贝是必要的。如果不那么做,调用者就可以轻易的破坏封装。

举例

比如下面 Planet 类,在构造函数和 get 函数中都做了保护性拷贝,这样外部调用者无法改变类内部状态,才能认为 Planet 是不可变对象。

import java.util.Date;

/**
* Planet is an immutable class, since there is no way to change
* its state after construction.
*/
public final class Planet {

  public Planet (double aMass, String aName, Date aDateOfDiscovery) {
     fMass = aMass;
     fName = aName;
     //make a private copy of aDateOfDiscovery
     //this is the only way to keep the fDateOfDiscovery
     //field private, and shields this class from any changes that
     //the caller may make to the original aDateOfDiscovery object
     fDateOfDiscovery = new Date(aDateOfDiscovery.getTime());
  }

  /**
  * Returns a primitive value.
  *
  * The caller can do whatever they want with the return value, without
  * affecting the internals of this class. Why? Because this is a primitive
  * value. The caller sees its "own" double that simply has the
  * same value as fMass.
  */
  public double getMass() {
    return fMass;
  }

  /**
  * Returns an immutable object.
  *
  * The caller gets a direct reference to the internal field. But this is not
  * dangerous, since String is immutable and cannot be changed.
  */
  public String getName() {
    return fName;
  }

//  /**
//  * Returns a mutable object - likely bad style.
//  *
//  * The caller gets a direct reference to the internal field. This is usually dangerous,
//  * since the Date object state can be changed both by this class and its caller.
//  * That is, this class is no longer in complete control of fDate.
//  */
//  public Date getDateOfDiscovery() {
//    return fDateOfDiscovery;
//  }

  /**
  * Returns a mutable object - good style.
  *
  * Returns a defensive copy of the field.
  * The caller of this method can do anything they want with the
  * returned Date object, without affecting the internals of this
  * class in any way. Why? Because they do not have a reference to
  * fDate. Rather, they are playing with a second Date that initially has the
  * same data as fDate.
  */
  public Date getDateOfDiscovery() {
    return new Date(fDateOfDiscovery.getTime());
  }

  // PRIVATE

  /**
  * Final primitive data is always immutable.
  */
  private final double fMass;

  /**
  * An immutable object field. (String objects never change state.)
  */
  private final String fName;

  /**
  * A mutable object field. In this case, the state of this mutable field
  * is to be changed only by this class. (In other cases, it makes perfect
  * sense to allow the state of a field to be changed outside the native
  * class; this is the case when a field acts as a "pointer" to an object
  * created elsewhere.)
  *
  * In new code, you should use java.time classes, not java.util.Date.
  */
  private final Date fDateOfDiscovery;
}

2017-06-15 java , defensive-copy , linux , notes , effective-java

知乎上被删除的良心回答之油猴脚本

今天偶然间看到一个知乎问题:“最良心的软件可以良心到什么程度?”,当时在 Google+ 上点进去粗略看了一样,看到油猴脚本也就坦然了,而添加到稍后阅读,在晚上回来之后准备细看时却惊讶于知乎屏蔽的速度,在尝试使用 Google,bing 和 web archive ,baidu 的历史记录之后终于找回了一些渣滓。

我在很早就已经推荐过 Tampermonkey, 也借此推荐过 我用过的 Userscript 。 而 Tampermonkey 我也用了很多年也曾总结过 Tampermonkey 同步的功能。不过多少年过去了,很多脚本失效的失效,我自己之前写得找电影脚本也因为跨域问题,一直懒没有修复。

幸好看到这样一篇总结帖,看看这两年又更新出来多少新玩法。以下为从历史记录中搜出:

直接来看 油猴脚本能干啥:

  • 直接观看各大视频网站的VIP 视频
  • 去除各大视频广告,百度搜索广告
  • 免费收听网易云320k 高音质音乐
  • 高速下载百度云
  • 去除各种验证码

在国内互联网广告满天飞的现在,这些功能无疑会吸引很多的用户。

下面列举一些非常好用的脚本,我在 http://einverne.github.io/post/2015/08/userscripts.html 中列出的脚本不在更新。

网易云高音质下载

在网页上听网易高品质音乐,在网页歌曲页面播放按钮上面也能看到下载歌曲按钮,或者下载歌词,封面,MV 等等。

地址:https://greasyfork.org/zh-CN/scripts/23222

百度网盘直接下载助手

这个插件,你可以使用它可以直接获取文件原始链接,这样你就可以使用第三方(IDM,Folx)下载了,再也不用使用百度云客户端的龟速了。 还可以多个文件选择,批量下载。

地址:https://greasyfork.org/zh-CN/scripts/23635

解决百度云大文件下载限制 https://greasyfork.org/zh-CN/scripts/17800

百度网盘的下载助手,一直在有效,失效,修复,有效,再失效的过程中,因为在 Linux 下很久不用百度云的客户端,而百度最近频频限制下载速度,并且在分享页面增加很多限制,只能使用客户端下载,这么多动作的背后,一方面是因为带宽和存储费用逐渐增高,却无奈找不到任何赢利点,另一方面也因为监管力度的加强。在网盘大战落下帷幕的时候市场上还能够坚持到最后的也就剩下百度,金山,乐视云,360,大大小小的众网盘纷纷宣布停止运营。百度在一家独大之后也是频频限制免费用户的行为,所以用这么多的脚本来提高百度的体验,还不如彻底的原理百度云盘。用自己的一点钱买一个 Dropbox 或者 Google Drive 反而要轻松很多呢。

一键离线下载

将网页上的磁力链接离线到网盘,在寻找电影的时候,这也是经常的动作,这个脚本将找电影,然后到离线的过程自动化了。

https://greasyfork.org/zh-CN/scripts/22590

免费看VIP 视频

直接在 greasyfork.org 搜索 VIP 即可。

授人以鱼不如授人以渔,每次看到此类 Collection 的时候总是会想起之前的那篇文章 文章中有总结如今非常活跃的几个脚本聚集地。根据关键词应该能够搜索到很多关心的内容。


2017-06-09 Chrome , userscript , tampermonkey

每天学习一个命令:traceroute 查看路由信息

traceroute(跟踪路由)是路由跟踪实用程序,用于确定 IP 数据报访问目标所经过的路径。traceroute 命令用 IP 生存时间 (TTL) 字段和 ICMP 错误消息来确定从一个主机到网络上其他主机的路由。

通过 traceroute 命令可以知道数据包从你的计算机到互联网另一端的主机是走的什么路径。当然每次数据包由某一同样的出发点(source)到达某一同样的目的地 (destination) 走的路径可能会不一样,但基本上来说大部分时候所走的路由是相同的。

linux 系统中是 traceroute, 在 MS Windows 中为 tracert。traceroute 通过发送小数据包到目的主机直到其返回,来测量其耗时。一条路径上的每个设备 traceroute 要测 3 次。输出结果中包括每次测试的时间 (ms) 和设备的名称及其 IP 地址。

在大多数情况下,我们会在 linux 主机系统下,直接执行命令行:

traceroute hostname

使用

命令格式:

traceroute 『参数』 『主机』

命令功能:

traceroute 预设数据包大小是 40Bytes,可另行设置。

具体参数格式:

traceroute [-dFlnrvx][-f 存活数值][-g 网关...][-i 网络界面][-m 存活数值][-p 通信端口][-s 来源地址][-t 服务类型][-w 超时秒数][主机名称或 IP 地址] 数据包大小

命令参数:

-d 使用 Socket 层级的排错功能。
-f 设置第一个检测数据包的存活数值 TTL 的大小。
-F 设置勿离断位。
-g 设置来源路由网关,最多可设置 8 个。
-i 使用指定的网络界面送出数据包。
-I 使用 ICMP 回应取代 UDP 资料信息。
-m 设置检测数据包的最大存活数值 TTL 的大小。
-n 直接使用 IP 地址而非主机名称。
-p 设置 UDP 传输协议的通信端口。
-r 忽略普通的 Routing Table,直接将数据包送到远端主机上。
-s 设置本地主机送出数据包的 IP 地址。
-t 设置检测数据包的 TOS 数值。
-v 详细显示指令的执行过程。
-w 设置等待远端主机回报的时间。
-x 开启或关闭数据包的正确性检验。

实例

最常用法

直接追踪路由

traceroute google.com

说明:

序列号从 1 开始,每个纪录就是一跳 ,每跳表示一个网关,每行有三个时间,单位是 ms,其实就是 -q 的默认参数。探测数据包向每个网关发送三个数据包后,网关响应后返回的时间;如果您用 traceroute -q 10 google.com ,表示向每个网关发送 10 个数据包。

traceroute 一台主机有时会看到一些行以星号表示,出现这样的情况,可能是防火墙封掉了 ICMP 的返回信息,所以得不到什么相关的数据包返回数据。

有时在某一网关处延时比较长,可能是某台网关比较阻塞,也可能是物理设备本身的原因。当然如果某台 DNS 出现问题时,不能解析主机名、域名时,也会有延时长的现象;您可以加 -n 参数来避免 DNS 解析,以 IP 格式输出数据。

如果在局域网中的不同网段之间,可以通过 traceroute 来排查问题所在,是主机的问题还是网关的问题。如果通过远程来访问某台服务器遇到问题时,用到 traceroute 追踪数据包所经过的网关,提交 IDC 服务商,也有助于解决问题;但目前看来在国内解决这样的问题是比较困难的,即使发现问题,IDC 服务商也不可能帮助解决。

跳数设置

traceroute -m 10 google.com

不解析主机名

traceroute -n google.com

设置探测包数量

traceroute -q 4 google.com

绕过正常的路由表直接发送到网络相连的主机

traceroute -r douban.com

工作原理

traceroute 命令利用 ICMP 及 IP header 的 TTL(Time To Live) 字段 (field)。

  • traceroute 送出一个 TTL 是 1 的 IP datagram 到目的地(其实,每次送出的为 3 个 40 字节的包,包括源地址,目的地址和包发出的时间标签),当路径上的第一个路由器 (router) 收到这个 datagram 时,它将 TTL 减 1。此时,TTL 变为 0 了,所以该路由器会将此 datagram 丢掉,并送回一个「ICMP time exceeded」消息(包括发 IP 包的源地址,IP 包的所有内容及路由器的 IP 地址),traceroute 收到这个消息后,便知道这个路由器存在于这个路径上
  • 接着 traceroute 再送出另一个 TTL 是 2 的 datagram,发现第 2 个路由器
  • ……
  • traceroute 每次将送出的 datagram 的 TTL 加 1 来发现另一个路由器,这个重复的动作一直持续到某个 datagram 抵达目的地。当 datagram 到达目的地后,该主机并不会送回 ICMP time exceeded 消息,因为它已是目的地了。
  • 那么 traceroute 如何得知目的地到达了呢?traceroute 在送出 UDP datagrams 到目的地时,它所选择送达的 port number 是一个一般应用程序都不会用的端口 (30000 以上),所以当此 UDP datagram 到达目的地后该主机会回送一个 (ICMP port unreachable) 的消息,而当 traceroute 收到这个消息时,便知道目的地已经到达了。所以 traceroute 在 Server 端也是没有所谓的 Daemon 程式。

traceroute 提取发 ICMP TTL 到期消息设备的 IP 地址并作域名解析。每次 traceroute 都打印出一系列数据,包括所经过的路由设备的域名及 IP 地址,三个包每次来回所花时间。


2017-06-09 traceroute , network , linux , command

Ubuntu/Debian 安装 nginx

Nginx 是非常流行的 HTTP/HTTPS 服务器软件,它也可以作为反向代理服务器,邮件代理服务器,可以用于负载均衡,缓存等等。

基本的 Nginx 由 master 进程和 worker 进程组成, master 读取配置文件,并维护 worker 进程,而 worker 会对请求进行处理。

Nginx 有两个主要的分支可供安装,stable 和 mainline 。这两个分支的主要区别可以从下图看出:

nginx two branchs

stable 分支并不意味着比 mainline 更加稳定可靠,事实上 mainline 更加稳定,因为 Nginx 开发人员会把所有的 bugfixes 都提交到该分支,而只会把 major bugfixes 提交到 stable 分支。然而另一方面,在 stable 分支的提交很少会影响到第三方模块,而在 mainline 上面的开发可能更快所有的新特性,更新,bugs,都会可能对第三方模块造成影响。

Nginx 官方 建议可以在任何时候使用 mainline 分支。而在生产环境使用 stable 分支。

安装

Use following command to install:

sudo apt-get install nginx
nginx -V

all config file is under /etc/nginx/nginx.conf

all vhost is under /etc/nginx/sites-available

program file is under /usr/sbin/nginx

log file is under /var/log/nginx , name of log file is access.log and error.log

init script has been created under /etc/init.d/

start from nginx 1.4.1, the default vhost direcotory is under /usr/share/nginx/html/

apt-get install nginx the config file is under /etc/nginx/site-available/default/

user data can be found in conf file.

sudo nginx -t to test and print log.

管理 nginx

start nginx

sudo service nginx start

stop nginx

sudo service nginx stop

other parameters:

reload        restart       start         status        stop

nginx 的配置文件及路径

托管网站内容 content

/usr/share/nginx/html/: actual web content, this path can be changed by altering Nginx configuration file.

默认 Ubuntu 16.04 会将 nginx 托管的地址指向 /var/www/html/ 目录。

服务配置 server configuration

Nginx 的主要配置都集中在 /etc/nginx 目录下:

/etc/nginx: The nginx configuration directory. All of the configuration files reside here.

  • /etc/nginx/sites-available/: The directory where per-site “server blocks” can be stored. Nginx will not use the configuration files found in this directory unless they are linked to the sites-enabled directory (see below). Typically, all server block configuration is done in this directory, and then enabled by linking to the other directory.

  • /etc/nginx/sites-enabled/: The directory where enabled per-site “server blocks” are stored. Typically, these are created by linking to configuration files found in the sites-available directory.

日志文件 log

/var/log/nginx/access.log: Every request to your web server is recorded in this log file unless Nginx is configured to do otherwise.

/var/log/nginx/error.log: Any Nginx errors will be recorded in this log.

nginx conf

nginx conf

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 768;
    # multi_accept on;
}

user Defines which Linux user will own and run the nginx. Most Debian-based distributions use www-data.

worker_process Defines how many threads, or simultaneous instances, of nginx to run. Learn more here

pid Defines where nginx will write its master process ID, or PID.

设置 Nginx Server Blocks

Server Blocks 类似 Apache Virtual Hosts(虚拟主机) 概念,作用就是通过配置让同一台机器同时托管多个域名。

首先创建目录

sudo mkdir -p /var/www/www.einverne.info/html
sudo chmod -R 755 /var/www/

如果组和用户不是 www-data ,可以用 sudo chown -R www-data:www-data /var/www/www.einverne.info/html 来改变

默认情况下 nginx 包含一个默认的 server block 叫做 default , 创建其他 server block 的时候可以以它作为模板:

sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/www.einverne.info

然后修改该配置

server {
    listen 80;
    listen [::]:80;

    root /var/www/www.einverne.info/html;
    index index.html index.htm index.nginx-debian.html;

    server_name www.einverne.info;

    location / {
        try_files $uri $uri/ =404;
    }
}

修改 vim /etc/nginx/nginx.conf

http {
    . . .

    server_names_hash_bucket_size 64;

    . . .
}

最后需要 ln 启用新的虚拟主机

sudo ln -s /etc/nginx/sites-available/www.einverne.info /etc/nginx/sites-enabled/

使用 sudo nginx -t 来测试配置。

重启 sudo /etc/init.d/nginx reload 启用新配置。

更多的 Nginx 配置相关内容可以查看新文章 Nginx conf

从源代码编译 Nginx 安装

获取 Nginx 最新版本 http://nginx.org/en/download.html 使用最新 mainline 版本即可

下载最新源代码,解压

wget http://nginx.org/download/nginx-1.13.6.tar.gz && tar zxvf nginx-1.13.6.tar.gz

下载安装依赖

以下内容都使用 root 安装 su -

apt-get install -y gcc g++ make automake build-essential

安装 PCRE 库,Nginx Core 和 Rewrite 模块提供正则支持

apt-get install libpcre3 libpcre3-dev

OpenSSL

sudo apt-get install openssl libssl-dev libperl-dev

zlib 库,提供 Gzip 模块支持,压缩 headers

apt-get install -y zlib1g zlib1g-dev

XML xslt

apt-get install libxslt-dev

GD Library

apt-get install libgd2-dev

GeoIP Library

apt-get install libgeoip-dev

使用 APT 源安装 Nginx,并查看版本 nginx -V

nginx -V
nginx version: nginx/1.10.3 (Ubuntu)
built with OpenSSL 1.0.2g  1 Mar 2016
TLS SNI support enabled
configure arguments: --with-cc-opt='-g -O2 -fPIE -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -fPIE -pie -Wl,-z,relro -Wl,-z,now' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-debug --with-pcre-jit --with-ipv6 --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_addition_module --with-http_dav_module --with-http_geoip_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_image_filter_module --with-http_v2_module --with-http_sub_module --with-http_xslt_module --with-stream --with-stream_ssl_module --with-mail --with-mail_ssl_module --with-threads

Configure 后面的参数在编译时会需要用到

在反向代理中替换原网页内容,需要在编译时加入第三方模块 substitution

git clone https://github.com/yaoweibin/ngx_http_substitutions_filter_module

另一个方便快捷配置 Google 反代的模块

git clone https://github.com/cuber/ngx_http_google_filter_module

然后进入 Nginx 源代码目录,注意参数中 --add-module 后面需要加入上面提及的两个 module 路径:

cd nginx-1.13.6/
./configure \
--with-cc-opt='-g -O2 -fPIE -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -fPIE -pie -Wl,-z,relro -Wl,-z,now' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-debug --with-pcre-jit --with-ipv6 --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_addition_module --with-http_dav_module --with-http_geoip_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_image_filter_module --with-http_v2_module --with-http_sub_module --with-http_xslt_module --with-stream --with-stream_ssl_module --with-mail --with-mail_ssl_module --with-threads \
--add-module=../ngx_http_substitutions_filter_module \
--add-module=../ngx_http_google_filter_module

设置后,开始检查编译参数和环境,如果少了某些安装包,或者需要特定版本的 lib 就会报错,Google 一下需要的依赖包安装即可。安装之后再次 ./configure

检查通过显示

Configuration summary
  + using threads
  + using system PCRE library
  + using system OpenSSL library
  + using system zlib library

  nginx path prefix: "/usr/share/nginx"
  nginx binary file: "/usr/sbin/nginx"
  nginx modules path: "/usr/share/nginx/modules"
  nginx configuration prefix: "/etc/nginx"
  nginx configuration file: "/etc/nginx/nginx.conf"
  nginx pid file: "/run/nginx.pid"
  nginx error log file: "/var/log/nginx/error.log"
  nginx http access log file: "/var/log/nginx/access.log"
  nginx http client request body temporary files: "/var/lib/nginx/body"
  nginx http proxy temporary files: "/var/lib/nginx/proxy"
  nginx http fastcgi temporary files: "/var/lib/nginx/fastcgi"
  nginx http uwsgi temporary files: "/var/lib/nginx/uwsgi"
  nginx http scgi temporary files: "/var/lib/nginx/scgi"

./configure: warning: the "--with-ipv6" option is deprecated

然后编译

make
make install

然后将编译后的文件替换到发行版的安装目录

cp -rf objs/nginx /usr/sbin/nginx

检查 nginx -V 即可看到新编译的版本。

常用配置

Options Explanation
--prefix=<path> 安装的根目录,默认为 /usr/local/nginx
--sbin-path=<path> nginx 二进制文件路径,如果没有设定,则使用 prefix 作为相对路径
--conf-path=<path> 配置路径
--error-log-path=<path> 错误 log
--pid-path=<path> nginx 写 pid 文件,通常在 /var/run
--lock-path=<path> 共享内存锁文件
--user=<user> 在哪个用户下运行 worker processes
--group=<group>
--with-debug 开启 debug log 生产环境不要启用
--with-http_ssl_module 开启 HTTP SSL 模块,支持 HTTPS
--with-http_realip_module 开启真实来源 IP
--with-http_flv_module 开启 flash 视频流
--with-http_mp4_module 开启 H.264/AAC 文件视频流
--with-http_gzip_static_module 开启预压缩文件传前检查,防止文件被重复压缩
--with-http_gunzip_module 开启为不支持 gzip 的客户端提前解压内容
--with-http_stub_status_module 开启 nginx 运行状态
--with_http_substitutions_filter_module 开启替换原网页内容

reference


2017-06-06 linux , nginx , ubuntu , debian , web

MySQL 中 utf8 和 utf8mb4 区别

今天在插入 MySQL 时遇到如下错误

Incorrect string value: ‘\xF0\x9F\x98\x81…’ for column ‘data’ at row 1

查证之后发现是因为插入的时候字符串中有 emoji,而 emoji 是 unicode 编码,MySQL 当时在建表时选择了 utf8 编码,导致了上述错误。mysql 支持的 utf8 编码最大字符长度为 3 字节,如果遇到 4 字节的宽字符就会插入异常了,因此引出了 utf8mb4 编码。MySQL 在 5.5.3 之后增加了这个 utf8mb4 的编码,mb4 就是 most bytes 4 的意思,专门用来兼容四字节的 unicode。好在 utf8mb4 是 utf8 的超集,除了将编码改为 utf8mb4 外不需要做其他转换。当然,为了节省空间,一般情况下使用 utf8 也就够了。

为了获取更好的兼容性,应该总是使用 utf8mb4 而非 utf8,对于一般性要求建议普通表使用 utf8, 如果这个表需要支持 emoji 就使用 utf8mb4。

深入 Mysql 字符集设置

字符 (Character) 是指人类语言中最小的表义符号。例如’A’、’B’等;给定一系列字符,对每个字符赋予一个数值,用数值来代表对应的字符,这一数值就是字符的编码 (Encoding)。例如,我们给字符’A’赋予数值 0,给字符’B’赋予数值 1,则 0 就是字符’A’的编码;给定一系列字符并赋予对应的编码后,所有这些字符和编码对组成的集合就是字符集 (Character Set)。例如,给定字符列表为{‘A’,’B’}时,{‘A’=>0, ‘B’=>1}就是一个字符集;

字符序 (Collation) 是指在同一字符集内字符之间的比较规则;确定字符序后,才能在一个字符集上定义什么是等价的字符,以及字符之间的大小关系;每个字符序唯一对应一种字符集,但一个字符集可以对应多种字符序,其中有一个是默认字符序 (Default Collation);

MySQL 中的字符序名称遵从命名惯例:以字符序对应的字符集名称开头;以 _ci(表示大小写不敏感)、_cs(表示大小写敏感)或 _bin(表示按编码值比较)结尾。例如:在字符序 utf8_general_ci 下,字符 “a” 和“A”是等价的;

字符集相关命令

检测字符集问题的一些手段

  • SHOW CHARACTER SET;
  • SHOW COLLATION;
  • SHOW VARIABLES LIKE ‘character%’;
  • SHOW VARIABLES LIKE ‘collation%’;
  • 查看数据库的字符集 use dbname;SELECT @@character_set_database, @@collation_database;
  • 查看表的字符集 SHOW TABLE STATUS where name like 'table_name';
  • 查看表中列的字符集 SHOW FULL COLUMNS FROM table_name;

其他一些修改语句

# 修改数据库:
ALTER DATABASE database_name CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
# 修改表:
ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
# 修改表字段:
ALTER TABLE table_name MODIFY column_name VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE table_name CHANGE column_name column_name VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

2017-06-02 mysql , encoding , utf8 , unicode

Celery 最佳实践

如果你第一次听说 Celery,可以去看下之前的 Celery 介绍 了解下 Celery 的基本功能,然后再来看这篇文章。

尽量不要使用数据库作为 AMQP Broker

随着 worker 的不断增多可能给数据库 IO 和连接造成很大压力。更具体来说不要把 Celery 的 task 数据和应用数据放到同一个数据库中。 Docker 上很多 相关的镜像。

使用多个队列

对于不同的 task ,尽量使用不同的队列来处理。

@app.task()
def my_taskA(a, b, c):
	print("doing something here...")
@app.task()
def my_taskB(x, y):
	print("doing something here...")

celery_config.py 中定义

task_queues=(
    Queue('default', routing_key='default'),
    Queue('other', routing_key='other'),

在 task 上定义

@app.task(queue='other')
def parse_something():
	pass

定义具有优先级的 workers

假如有一个 taskA 去处理一个队列 A 中的信息,一个 taskB 去处理队列 B 中的数据,然后起了 x 个 worker 去处理队列 A ,其他的 worker 去处理队列 B。而这时也可能会出现队列 B 中一些 task 急需处理,而此时堆积在队列 B 中的 tasks 很多,需要耗费很长时间来处理队列 B 中的 task。此时就需要定义优先队列来处理紧急的 task。

celery 中可以在定义 Queue 时,指定 routing_key

Queue('other', routing_key='other_high'),
Queue('other', routing_key='other_low'),

然后定义

task_routes={
	# see
	# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_routes
	# http://docs.celeryproject.org/en/latest/userguide/routing.html#routing-basics
	'path.to.task' : {
		'queue': 'other',
		'routing_key': 'other_high'
	},
	'path.to.task' : {
		'queue': 'other',
		'routing_key': 'other_low'
	},
}

在启动 worker 时指定 routing_key

celery worker -E -l INFO -n workerA -Q other_high
celery worker -E -l INFO -n workerB -Q other_low

使用 celery 的错误处理机制

一般情况下可能因为网络问题,或者第三方服务暂时性错误而导致 task 执行出错。这时可以使用 celery task 的重试机制。

@app.task(bind=True, default_retry_delay=300, max_retries=5)
def my_task_A():
	try:
		print("doing stuff here...")
	except SomeNetworkException as e:
		print("maybe do some clenup here....")
		self.retry(e)

一般添加 default_retry_delay 重试等待时间和 max_retries 重试次数来限定,防止任务无限重试。

使用 Flower

Flower 项目 为监控 celery tasks 和 workers 提供了一系列的便利。他使用 Web 界面提供 worker 当前状态, task 执行进度,各个 worker 详细信息,甚至可以在网页上动态更行执行速率。

只有在真正需要时才去追踪 celery 的 result

任务的状态存储任务在退出时成功或者失败的信息,这些信息有些时候很重要,尤其是在后期分析数据时,但是大部分情况下更加关心 task 执行过程中真正想要保存的数据,而不是任务的状态。

所以,可以使用 task_ignore_result = True 来忽略任务结果。

不要将 Database/ORM 对象传入 tasks

不应该讲 Database objects 比如一个 User Model 传入在后台执行的任务,因为这些 object 可能包含过期的数据。相反应该传入一个 user id ,让 task 在执行过程中向数据库请求全新的 User Object。

以上七条来自:https://denibertovic.com/posts/celery-best-practices/

尽量简化 tasks

task 应该简洁 (concise):

  • 将主要 task 逻辑包含在对象方法或者方法中
  • 确保方法抛出明确的异常 (identified exceptions)
  • 只有在切当的时机再实现重试机制

假设需要实现一个发送邮件的 task

import requests
from myproject.tasks import app  # app is your celery application
from myproject.exceptions import InvalidUserInput
from utils.mail import api_send_mail
@app.task(bind=True, max_retries=3)
def send_mail(self, recipients, sender_email, subject, body):
	"""Send a plaintext email with argument subject, sender and body to a list of recipients."""
	try:
		data = api_send_mail(recipients, sender_email, subject, body)
	except InvalidUserInput:
		# No need to retry as the user provided an invalid input
		raise
	except Exception as exc:
		# Any other exception. Log the exception to sentry and retry in 10s.
		sentrycli.captureException()
		self.retry(countdown=10, exc=exc)
	return data

通常任务真实的实现只有一层,而剩余的其他部分都是错误处理。而通常这么处理会更加容易维护。

设置 task 超时

设置一个全局的任务超时时间

task_soft_time_limit = 600   # 600 seconds

超时之后会抛出 SoftTimeLimitExceeded 异常

from celery.exceptions import SoftTimeLimitExceeded
@app.task
def mytask():
	try:
		return do_work()
	except SoftTimeLimitExceeded:
		cleanup_in_a_hurry()

同样,定义任务时也能够指定超时时间,如果任务 block 尽快让其失败,尽量配置 task 的超时时间。不让长时间 block task 的进程。

@app.task(
	bind=True,
	max_retries=3,
	soft_time_limit=5 # time limit is in seconds.
)
def send_mail(self, recipients, sender_email, subject, body):
	...

将 task 重复部分抽象出来

使用 task 的基类来复用部分 task 逻辑

from myproject.tasks import app
class BaseTask(app.Task):
	"""Abstract base class for all tasks in my app."""
	abstract = True
	def on_retry(self, exc, task_id, args, kwargs, einfo):
		"""Log the exceptions to sentry at retry."""
		sentrycli.captureException(exc)
		super(BaseTask, self).on_retry(exc, task_id, args, kwargs, einfo)
	def on_failure(self, exc, task_id, args, kwargs, einfo):
		"""Log the exceptions to sentry."""
		sentrycli.captureException(exc)
		super(BaseTask, self).on_failure(exc, task_id, args, kwargs, einfo)

@app.task(
	bind=True,
	max_retries=3,
	soft_time_limit=5,
	base=BaseTask)
def send_mail(self, recipients, sender_email, subject, body):
	"""Send a plaintext email with argument subject, sender and body to a list of recipients."""
	try:
		data = api_send_mail(recipients, sender_email, subject, body)
	except InvalidUserInput:
		raise
	except Exception as exc:
		self.retry(countdown=backoff(self.request.retries), exc=exc)
	return data

将大型 task 作为类

一般情况下将使用方法作为 task 就已经足够,如果遇到大型 task ,可以将其写成

class handle_event(BaseTask):   # BaseTask inherits from app.Task
	def validate_input(self, event):
		...
	def get_or_create_model(self, event):
		...
	def stream_event(self, event):
		...
	def run(self, event):
		if not self.validate_intput(event):
			raise InvalidInput(event)
		try:
			model = self.get_or_create_model(event)
			self.call_hooks(event)
			self.persist_model(event)
		except Exception as exc:
			self.retry(countdown=backoff(self.request.retries), exc=exc)
		else:
			self.stream_event(event)

单元测试

直接调用 worker task 中的方法,不要使用 task.delay() 。 或者使用 Eager Mode,使用 task_always_eager 设置来启用,当启用该选项之后,task 会立即被调用。而 这两种方式都只能测试 task worker 中的内容,官方 1 并不建议这么做。

对于执行时间长短不一的任务建议开启 -Ofair

celery 中默认 都会有 prefork pool 会异步将尽量多的任务发送给 worker 执行,这也意味着 worker 会预加载一些任务。这对于通常的任务会有性能提升,但这也容易导致因为某一个长任务处理时间长,而导致其他任务处于长时间等待状态。

对于执行时间长短不一的任务可以开启 -Ofair

celery -A proj worker -l info -Ofair

设置 worker 的数量

Celery 默认会开启和 CPU core 一样数量的 worker,如果想要不想开启多个 worker ,可以通过启动时指定 --concurrency 选项

--concurrency=1

在 Celery 中使用多线程

上面提到使用 --concurrency=1 或者 -c 1 来设置 worker 的数量,Celery 同样支持 Eventlet 协程方式,如果你的 worker 有大量的 IO 操作,网络请求,那么此时使用 Eventlet 协程来提高 worker 的执行效率。确保在使用 Eventlet 之前对 Eventlet 非常了解,否则不要轻易使用

celery -A proj worker -P eventlet -c 10

reference


2017-05-21 celery , python , web , database , redis , queue

每天学习一个命令:sudo 来管理 Linux 下权限

sudo 表示 “superuser do”。它允许已验证的用户以其他用户的身份来运行命令。其他用户可以是普通用户或者超级用户。然而,绝大部分时候我们用它来以提升的权限来运行命令。

sudo 命令与安全策略配合使用,默认安全策略是 sudoers,可以通过编辑文件 /etc/sudoers 来配置。其安全策略具有高度可拓展性。人们可以开发和分发他们自己的安全策略作为插件。

sudo 与 su 的区别

在 GNU/Linux 中,有两种方式可以用提升的权限来运行命令:

  • su 命令
  • sudo 命令

su 表示 “switch user”。使用 su 命令,我们可以切换到 root 用户并且执行命令,但是这种方式存在一些缺点:

  • 需要与他人共享 root 的密码
  • 无法审查用户执行的命令
  • 对于 root 用户,不能授予有限的访问权限

sudo 以独特的方式解决了这些问题:

  • 不需要共享 root 用户的密码,普通用户可以使用自己的密码提升权限来执行命令
  • sudo 用户的所有操作都会被记录下来,管理员可以随时审查 sudo 用户执行了哪些操作
  • 可控的 sudo 用户的访问,我们可以限制用户只能执行某些命令

在基于 Debian 的 GNU/Linux 中,所有活动都记录在 /var/log/auth.log 文件中

使用 sudo

为普通用户添加 sudo 权限

/etc/sudoers 文件记录着谁可以用 sudo 命令来提升权限,添加普通用户为 sudo 用户

  1. 编辑 /etc/sudoers 文件最有效的方式是使用 visudo 命令:

     sudo visudo
    
  2. 添加以下行来允许用户 einverne 有 sudo 权限:

     einverne ALL=(ALL) ALL
     %admin  ALL=(ALL) ALL
    

上述命令中:

  • einverne 表示用户名,带有 %admin 的表示 admin 用户组授予 sudo 访问权限
  • 第一个 ALL 指示允许从任何终端、机器访问 sudo
  • 第二个 (ALL) 指示 sudo 命令被允许以任何用户身份执行
  • 第三个 ALL 表示所有命令都可以作为 root 执行

如果打开默认的 visudo 可以发现已经配置这样一行

root 	ALL=(ALL:ALL)  ALL

这一行表示的含义是,用户 root, 登录任何 hostname, 可以在任何用户或者组下运行任何命令。

user hostname=(runas-user:runas-group) command

这里有一个需要稍微注意的地方,括号中能看到有 (ALL)(ALL:ALL) 的区别,如果仅仅使用 (ALL) 那么,sudo 无法使用 -g 来指定用户组运行程序。1

如果发现在该配置中已经配置了 %admin ALL=(ALL) ALL,那么把用户加入到该 admin 组中,就不需要另外配置该用户了。

usermod -aG sudo einverne

注:/etc/sudoers 文件必须以 visudo 命令来修改,该命令可以防止因为文件格式错误而导致问题,如果visudo默认的编辑器不是你常用的编辑,可以通过如下方法来修改

sudo update-alternatives --config editor

将用户添加到 sudo 组

上面提到编辑 /etc/sudoers 文件给用户添加 sudo 权限,不过还有一种更加简单的方法就是将用户添加到 sudo 或者 admin 组,修改 /etc/group 文件,然后在其中添加

sudo:x:27:username,username1

然后该 username 用户提升到 sudo 组(默认 sudo 组应该在 /etc/sudoers 中配置好权限)即可使用 sudo 命令。

使用 sudo 提升权限执行命令

要用提升的权限执行命令,只需要在命令前加上 sudo,如下所示:

sudo cat /etc/passwd

当你执行这个命令时,它会询问用户 einverne 的密码,而不是 root 用户的密码。

使用 sudo 以其他用户身份执行命令

除此之外,我们可以使用 sudo 以另一个用户身份执行命令。例如,在下面的命令中,用户 einverne 以用户 demo 的身份执行命令:

sudo -u demo whoami
[sudo] password for einverne:
demo

内置命令行为

sudo 的一个限制是 —— 它无法使用 Shell 的内置命令。例如, history 记录是内置命令,如果你试图用 sudo 执行这个命令,那么会提示如下的未找到命令的错误:

sudo history
[sudo] password for einverne:
sudo: history: command not found

为了克服上述问题,我们可以访问 root shell,并在那里执行任何命令,包括 Shell 的内置命令。

要访问 root shell, 执行下面的命令:

sudo bash

执行完这个命令后——您将观察到提示符变为井号(#)。

技巧

这里将列举一些常用的 sudo 小技巧,可以用于日常任务。

以 sudo 用户执行之前的命令

让我们假设你想用提升的权限执行之前的命令,那么下面的技巧将会很有用:

sudo !4

上面的命令将使用提升的权限执行历史记录中的第 4 条命令。

在 Vim 里面使用 sudo 命令

很多时候,我们编辑系统的配置文件时,在保存时才意识到我们需要 root 访问权限来执行此操作。因为这个可能让我们丢失我们对文件的改动,我们可以在 Vim 中使用下面的命令来解决这种情况:

:w !sudo tee %

上述命令中:

  • 冒号 (:) 表明我们处于 Vim 的退出模式
  • 感叹号 (!) 表明我们正在运行 shell 命令
  • sudo 和 tee 都是 shell 命令
  • 百分号 (%) 表明从当前行开始的所有行

使用 sudo 执行多个命令

至今我们用 sudo 只执行了单个命令,但我们可以用它执行多个命令。只需要用分号 (;) 隔开命令,如下所示:

sudo -- bash -c 'pwd; id;'

上述命令中

  • 双连字符 (–) 停止命令行切换
  • bash 表示要用于执行命令的 shell 名称
  • -c 选项后面跟着要执行的命令

查看 sudo 可以使用的命令

使用 -l 参数可以用来查看当前用户可执行的 sudo 命令

sudo -l

运行 sudo 命令时免去输入密码

当第一次执行 sudo 命令时,它会提示输入密码,默认情形下密码会被缓存 15 分钟。可以使用 NOPASSWD 关键字来禁用密码认证:

# User privilege specification
username ALL=(ALL) NOPASSWD: ALL 单独配置一个用户

# Allow members of group sudo to execute any command
%sudo ALL=(ALL) NOPASSWD:ALL 配置一组用户

解释一下该文件可以发现,每一行定义了一个配置,用户组需要使用 % 来区分。

限制 sudo 用户执行某些命令

为了提供受控访问,我们可以限制 sudo 用户只能执行某些命令。例如,下面的行只允许执行 echo 和 ls 命令 。

einverne ALL=(ALL) NOPASSWD: /bin/echo /bin/ls

深入了解 sudo

让我们进一步深入了解 sudo 命令。

ls -l /usr/bin/sudo
-rwsr-xr-x 1 root root 145040 Jun 13  2017 /usr/bin/sudo

如果仔细观察文件权限,则发现 sudo 上启用了 setuid 位。当任何用户运行这个二进制文件时,它将以拥有该文件的用户权限运行。在所示情形下,它是 root 用户。

为了显示这一点,我们可以使用 id 命令,如下所示:

id
uid=1002(einverne) gid=1002(einverne) groups=1002(einverne)

当我们不使用 sudo 执行 id 命令时,将显示用户 einverne 的 id。

sudo id
uid=0(root) gid=0(root) groups=0(root)

但是,如果我们使用 sudo 执行 id 命令时,则会显示 root 用户的 id。

reference


2017-05-16 linux , sudo , security , permission , privilege , command

电子书

最近文章

  • 每天学习一个命令:使用 rz sz 向服务器发送文件 搜索 rz sz 命令使用方式进来的,可以不用往下看了,直接学习 scp 或者 rsync 吧, rz sz 看了一下还是有很多限制的。
  • 分析家里局域网 WiFI 瓶颈 目前我的情况是,家中有一个千兆主路由放在客厅接外面的宽带,而我自己的房间有一台比较老的 Netgear 3800 路由来无线桥接连接外面的主路由,因为我不想我的 3800 路由中的设备暴露到主路由的设置中,所以用了 OpenWrt 的桥接模式。但是随着我在 3800 这台路下接的设备增多,导致目前 3800 这台路由不堪重负,已经影响到了我日常 streaming 局域网 NAS 中的电影。所以最近想更换一下这台已经 8 年历史的 WNDR 3800 路由器。
  • GitLab CI 使用笔记 CI/CD 不必多说。
  • 使用命令行远程网络唤起主机 在 Linux 下可以通过 etherwake 命令来网络唤醒设备。
  • Cloud-init 初始化虚拟机配置 在安装 Proxmox 后在它的文档中了解到了 cloud-init。所以就来梳理一下。