简单说说jsonp原理

前几天参加面试,好几个面试者简历都写着jsonp,解决跨域之类的调用。于是问一下知不知道jsonp的实现原理,结果没一个答得上来,有点小失望。

这里简单描述一下关键点,权当一个记录。

假设a网页调用b网站的服务

  • a网站会准备一个方法,例如callme(args)
  • a网站在页面插入一个script标签,src指向b网站的地址,并带上callme作为参数
  • b网站处理后,把结果和回调方法的名字组成一个字符串返回,例如callme(‘ok’)
  • 由于是script标签,所以字符串会被当成js解析执行,相当于调用到了callme方法
  • 主要利用了script可以跨站点访问的特性,且只能用GET请求,需要服务端做点配合,并且需要信任服务器(安全考虑)。jquery的jsonp ajax只是封装了这个过程,让你看上去和普通ajax没什么区别,其实却一点关系都没有。

jsonp这种小魔法的原理,网上一搜就可以找到,还是要有点好奇心的。

浅谈编程中的位操作

昨天有同学谈起网络编程中各种奇怪的位运算,所以单独整理一下实践中的位运算,权当复习。

基本的位运算 位运算符包括与,或,非,异或,这属于common sence了,只要给出一段01二进制串,每个程序员应该都可以处理。

类型

无论编程语言中的类型怎样,只要涉及网络通信,存储,最终都会涉及一个字节的转换过程。对于java来说,主要考虑整型,浮点型,字符串,其中整型和字符串是最常用的。

  • 整型,在java都是有符号数,根据不同类型都有固定的字节长度,并采用补码形式。关于补码,可以看看这篇文章关于2的补码
  • 浮点型,包括单精度和双精度,采用IEEE的浮点表示法,比较少使用,可以通过深入理解计算机系统2.4.2 IEEE浮点表示,这个章节了解浮点数的二进制形式。
  • 字符串,java的字符串采用unicode字符表示,存储的时候通常得考虑字符编码,所谓字符编码,就是字符和字节的对应关系。有些api会允许直接写入或读取字符串,但是对底层采用的字符编码要心里有底。

大端法,小端法

例如一个数,0x12345678, 存储到0x01到0x04这4个字节,那么有2种方式,一种是12345678,叫大端法,一种是78563412,叫小端法。就是看最低有效位(78)是在高地址(04)还是低地址(01)。这方面找了个文章理解字节序,可以了解一下。

tcpip协议规定网络传输统一采用大端法。指的是ip,tcp等协议的头部信息。见TCP/IP详解5.2章节 应用层网络协议有时会看到这个描述,没说明的话,默认也是按大端法开发,比较适应阅读。

数据转换

应用层协议上经常会涉及一些数据位的转换,但是有时不能类型强制转换,例如:某个协议上用一个无符号byte,例如b,那么可以通过b & 0xff得到一个int。这个类型强转的区别在于它会保留符号位。

浅谈系统线程数限制

Linux进程与线程

概念就不提了,Richard Stevens的描述:

fork is expensive. Memory is copied from the parent to the child, all descriptors are duplicated in the child, and so on. Current implementations use a technique called copy-on-write, which avoids a copy of the parent’s data space to the child until the child needs its own copy. But, regardless of this optimization, fork is expensive. IPC is required to pass information between the parent and child after the fork. Passing information from the parent to the child before the fork is easy, since the child starts with a copy of the parent’s data space and with a copy of all the parent’s descriptors. But, returning information from the child to the parent takes more work. Threads help with both problems. Threads are sometimes called lightweight processes since a thread is “lighter weight” than a process. That is, thread creation can be 10–100 times faster than process creation. All threads within a process share the same global memory. This makes the sharing of information easy between the threads, but along with this simplicity comes the problem.

Linux中创建进程用fork操作,线程用clone操作。通过ps -ef看到的是进程列表,线程可以通过ps -eLf来查看。 用top命令的话,通过H开关也可以切换到线程视图。

具体到Java线程模型,规范是没有规定Java线程和系统线程的对应关系的,不过目前常见的实现是一对一的。 参考http://openjdk.java.net/groups/hotspot/docs/RuntimeOverview.html#Thread%20Management|outline

问题排查思路

如果创建不了Java线程,报错是

Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread

下面是常见的问题原因:

内存太小

在Java中创建一个线程需要消耗一定的栈空间,默认的栈空间是1M(可以根据应用情况指定-Xss参数进行调整),栈空间过小或递归调用过深,可能会出现StackOverflowError。

对于一个进程来说,假设一定量可使用的内存,分配给堆空间的越多,留给栈空间的就越少。这个限制常见于32位Java应用,进程空间4G,用户空间2G(Linux下3G,所以通常堆可以设置更大一些),减去堆空间大小(通过-Xms、-Xmx指定范围),减去非堆空间(其中永久代部分通过PermSize、MaxPermSize指定大小,在Java8换成了MetaSpace,默认不限制大小),再减去虚拟机自身消耗,剩下的就是栈空间,假设剩下300M,那么理论上就限制了只能开300线程。不过对于64位应用,由于进程空间近乎无限大,所以可以不考虑这个问题。

ulimit限制

线程数还会受到系统限制,系统限制通过ulimit -a可以查看到。

https://ss64.com/bash/ulimit.html

caixj@Lenovo-PC:~$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7823
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7823
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

相关的限制有

max memory size - 最大内存限制,在64位系统上通常都设置成unlimited  
max user processes - 每用户总的最大进程数(包括线程) 
virtual memory - 虚拟内存限制,在64位系统上通常都设置成unlimited 

这些参数可以通过ulimit命令(当前用户临时生效)或者配置文件/etc/security/limits.conf(永久生效)进行修改。   检查某个进程的限制是否生效,可以通过/proc/PID/limits查看运行时状态。

参数sys.kernel.threads-max限制

https://www.kernel.org/doc/Documentation/sysctl/kernel.txt

This value controls the maximum number of threads that can be created
using fork().

During initialization the kernel sets this value such that even if the
maximum number of threads is created, the thread structures occupy only
a part (1/8th) of the available RAM pages.

The minimum value that can be written to threads-max is 20.
The maximum value that can be written to threads-max is given by the
constant FUTEX_TID_MASK (0x3fffffff).
If a value outside of this range is written to threads-max an error
EINVAL occurs.

The value written is checked against the available RAM pages. If the
thread structures would occupy too much (more than 1/8th) of the
available RAM pages threads-max is reduced accordingly.

表示系统全局的总线程数限制。设置方式有:

# 方式1 运行时限制,临时生效
echo 999999 > /proc/sys/kernel/threads-max
# 方式2 修改/etc/sysctl.conf,永久生效
sys.kernel.threads-max = 999999

参数sys.kernel.pid_max限制

https://www.kernel.org/doc/Documentation/sysctl/kernel.txt

PID allocation wrap value.  When the kernel's next PID value
reaches this value, it wraps back to a minimum PID value.
PIDs of value pid_max or larger are not allocated.

表示系统全局的PID号数值的限制。设置方式有:

# 方式1 运行时限制,临时生效
echo 999999 > /proc/sys/kernel/pid_max
# 方式2 修改/etc/sysctl.conf,永久生效
sys.kernel.pid_max = 999999

参数sys.vm.max_map_count限制

https://www.kernel.org/doc/Documentation/sysctl/vm.txt

This file contains the maximum number of memory map areas a process
may have. Memory map areas are used as a side-effect of calling
malloc, directly by mmap, mprotect, and madvise, and also when loading
shared libraries.

While most applications need less than a thousand maps, certain
programs, particularly malloc debuggers, may consume lots of them,
e.g., up to one or two maps per allocation.

The default value is 65536.

表示单个程序所能使用内存映射空间的数量限制。设置方式有:

# 方式1 运行时限制,临时生效
echo 999999 > /proc/sys/vm/max_map_count
# 方式2 修改/etc/sysctl.conf,永久生效
sys.vm.max_map_count = 999999

在其他资源可用的情况下,单个vm能开启的最大线程数是这个值的一半,可以通过cat /proc/PID/maps | wc -l查看目前使用的映射数量。

至于为什么只有一半,结合一些材料和源码分析了一下:

常见的警告信息是这样的,见JavaThread::create_stack_guard_pages()

Attempt to protect stack guard pages failed.
Attempt to deallocate stack guard pages failed. 

见current_stack_region()的图示,结合一下R大的相关解释:http://hllvm.group.iteye.com/group/topic/37717

如下图所示,通常的Java线程,会包括一个glibc的guard page和HotSpot的guard pages,其中JavaThread::create_stack_guard_pages()就是创建HotSpot Guard Pages用的,这里正常应该会有2次VMA,所以最大值只能有一半,从/proc/PID/maps中也可以看到增加一个线程会增加2个地址相连的映射空间。

// Java thread:
//
//   Low memory addresses
//    +------------------------+
//    |                        |\  JavaThread created by VM does not have glibc
//    |    glibc guard page    | - guard, attached Java thread usually has
//    |                        |/  1 page glibc guard.
// P1 +------------------------+ Thread::stack_base() - Thread::stack_size()
//    |                        |\
//    |  HotSpot Guard Pages   | - red and yellow pages
//    |                        |/
//    +------------------------+ JavaThread::stack_yellow_zone_base()
//    |                        |\
//    |      Normal Stack      | -
//    |                        |/
// P2 +------------------------+ Thread::stack_base()
//
// Non-Java thread:
//
//   Low memory addresses
//    +------------------------+
//    |                        |\
//    |  glibc guard page      | - usually 1 page
//    |                        |/
// P1 +------------------------+ Thread::stack_base() - Thread::stack_size()
//    |                        |\
//    |      Normal Stack      | -
//    |                        |/
// P2 +------------------------+ Thread::stack_base()
//
// ** P1 (aka bottom) and size ( P2 = P1 - size) are the address and stack size returned from
//    pthread_attr_getstack()

cgroup限制

现在新点的操作系统采用systemd的init程序,支持cgroup控制特性。docker的资源隔离底层技术就是这个。

其中有个重要的限制就是最大任务数TasksMax,通过设置cgroup的pids.max来限制。例如suse sp2的发行说明,见https://www.suse.com/releasenotes/x86_64/SUSE-SLES/12-SP2/#fate-320358

If you notice regressions, you can change a number of TasksMax settings.

To control the default TasksMax= setting for services and scopes running on the system, use the system.conf setting DefaultTasksMax=. This setting defaults to 512, which means services that are not explicitly configured otherwise will only be able to create 512 processes or threads at maximum.

For thread- or process-heavy services, you may need to set a higher TasksMax value. In such cases, set TasksMax directly in the specific unit files. Either choose a numeric value or even infinity.

Similarly, you can limit the total number of processes or tasks each user can own concurrently. To do so, use the logind.conf setting UserTasksMax (the default is 12288).

nspawn containers now also have a TasksMax value set, with a default of 16384.

上面的描述,说明

对于登录会话,有个默认的限制UserTasksMax,配置在/etc/systemd/logind.conf,限制了某个用户的默认的总任务数,例如上面限制了最大12288,修改这个配置文件可以通过systemctl restart systemd-logind重新加载

对于服务来说,配置在/etc/systemd/system.conf的DefaultTasksMax参数,默认是512(不同的发行版很可能不一样),如果需要定制,需要根据服务独立配置  

上面提到的是cgroup的默认全局设置,也可以细化到某个进程的限制。具体功能可以参考Linux Cgroup系列(03):限制cgroup的进程数(subsystem之pids)

通过find /sys/fs/cgroup -name “pids.max” 可以看到各种细化的配置,例如./pids/user.slice/user-1000.slice/pids.max就是id为1000的用户的限制,相当于覆盖了上面logind.conf的默认设置,修改这个值会立即生效。

要查看某个进程的具体限制,可以通过/proc/PID/cgroup查看运行时状态,其中里边有pids.max就是对应的限制情况。详细点的可以看看这个案例:https://zhuanlan.zhihu.com/p/29192624

框架应用中如何更好地解决问题

现代应用开发中或多或少会使用到各种开源,自研的库,框架等,各种资料良莠不齐, 除了部分成熟开源,商业框架的文档做得很好,使用广泛,生态完整,的确很多问题可以处理。 但自研的通常就是重灾区,资料少,不同步, 那么如何在这类这类框架中找到问题处理的方向,甚至解决方案呢?

昨天帮同事处理了一个uee框架使用的问题,可做借鉴。

1.对框架有个基本了解,例如框架的大体结构,层次,work流程。 对于各个部分如何work这块我认为是很关键的, 例如以安全界大哥大struts2为例,理解一个action的生成,要配置哪些文件, 这些文件怎么配合,生效路径是怎样的,和其他框架如spring怎么结合, 整个过程可以能在脑子里放电影式过一遍,当然能高清就最好了。 这个主要是建立框架的使用模型,方便和其他框架进行类比,解决一些基本的使用问题, 并为后续深入打好基础。

2.了解框架的实现原理,假设你有打开uee对应的生成件脚本文件, 就会发现里边有5w行js,连ide都会卡顿一下,怎么破?其实还是有门路的,因为uee是基于ng1的框架, 这个文件里边有一大半是jq和ng1的代码,后面部分才是扩展功能。 所以掌握ng1的框架原理至关重要,例如dirty check,directive这些黑科技, 以昨天的问题为例,就是直接进到对应directive的实现中,对比行为差异, 最后可以发现多配置一个参数就可以解决问题。 理解关键特性的实现原理,这样在调试验证过程中才心里有底,不会瞎蒙。

3.工欲善其事,必先利其器,熟练掌握各种工具,小到调试技巧,大到各种辅助工具。 调试技巧,例如上面的问题调试,一次触发会数十次call,一个个查看很容易miss target,这个时候就应该用条件断点。 类似的技巧还有异常断点(查找某个特定异常),赋值(改变运行路径)等。 如果大段代码不知哪里有bug,可以两分搜索,在断点处验证正确性。 调试不是用来找bug的,它只是验证的过程中顺手找到bug。 至于辅助工具,反编译工具,抓包工具,各种shell命令,脚本语言,技多不压身,总有合适的。

4.注意积累,适时总结。很多人不是厉害,只是踩坑多而已。 所以还是要多填坑,多填别人的坑,这样就显得很厉害了,填坑机会就更多了,这是个良性循环。 当然万事开头难,一开始还是要注重编程规范学习,看点书, 遇到问题的时候多找找资料,争取系统的过一下相关知识点。 例如你今天遇到了classdefnotfoundexception,问题解决了, 那么就看看这种异常有哪些情况下会出现,怎么避免,和其他相关异常有什么区别? 可能有很多人已经知道了,写出来很low,但做一下总结总是有好处的,日积月累,做好填坑的准备是关键。

书写节奏有点乱,非喜勿喷。今日@广州地铁。

关于攻城狮技术能力提升的一点看法

今天听到有同事说起,网上有技能培训的网课,视频教学,讲解一些’高级’技术,例如从头构建一个tomcat,实时演示。网课价格数千块,对于搞毕业没多久的同学,是一笔不小的开支。

对于这个事情,我是这么看的:

  1. 网课不一定和你的学习能力同步,教学嘛,总是要照顾大多数人,有些人会觉得太快,有些人却嫌慢。所以最好还是可以有针对性,系统性的自学。
  2. 高大上的技术不一定像表面看的那么好。说自己有搞hadoop,却MapReduce都不知道的我也见过。基础说白了,打好扎实的基础才是关键,高大上的技术按需分配。
  3. 实用软件通常都很复杂,有很多corner case需要处理,架构很大可能是比较清晰的,但是代码绝对不是复杂度小于5的。细节是魔鬼,还好大多数情况只要了解关键设计即可,这块通过一些设计文档,讲座就可以了解到。
  4. 知行合一才是理想的做法,我认为有些同学可能实践太多了,知识积累不够,’自己做的东西不够高大上’这种感觉非常突出,迫切需要掌握高大上的技术。或许我们做的东西是有点low,但也不是毫无价值可言,重要还是平时有没有注意积累。

说到知行合一,的确是攻城狮技术能力进阶的关键,一方面可以在项目中挖掘,一方面可以业余时间造轮子,即使非常简陋。

在项目中挖掘,以一个普通的web工程为例,用了servlet,那么这个规范说了些什么,有没有规范文档可以看看?用到了spring,那么它是怎么生成对象的,会用到哪个反射相关的api?一个web因为是基于http的,那么这个协议细节是怎样的?这些都是比较大的,还有很多小的,例如有些地方用了线程本地变量,这个东西有什么缺点?为什么有些地方用encodrURI,有些又是getBytes,还用不同的字符编码?有人用了ConcurrentHashMap,它是怎么实现的?还有哪些线程安全的工具类?看到很多代码用StringBuffer,为什么不是用StringBuilder?在finally里边直接关闭两个流,为什么findbugs会警告?

如果觉得上面的内容脱离实际,那么就造个轮子吧,挑个简单点的,就静态服务器吧,概念自己脑补,那需要怎么做呢?

既然是个服务器,那么得给人访问吧,需要支持个http协议,怎么破?噢,可以用socket编程,然后实现http协议,静态服务器嘛,先支持最简单的Get吧。 现在有个服务了,从Get请求路径中获取要访问的文件路径,然后拼接上指定的根目录,就知道文件地址了,在用文件读取后返回就可以了。

大功告成。如果上2步都可以顺利完成,的确是接近一个玩具的样子了。接下来补充一些功能。

  • 如果你是用阻塞io的话,那么总会涉及多线程,那么是否需要考虑线程池?
  • http请求的连接什么时候关闭?一请求一关闭虽然符合规范,但是keepalive是不是更好点?
  • 请求有可能超时,应该怎么处理?
  • 根目录总是不能写死吧,端口不能写死吧,应该搞成配置,是否考虑一下hot reload?
  • 每次请求都读取文件,看上去比较low,是不是要加个缓存,又要考虑内存占用,是不是考虑LRU?
  • 考虑一下安全吧,避免目录遍历?
  • http得考虑一下客户端缓存吧,例如expired,last_modified,tag等特性吧

看上去是有个玩具的样子了,如果都能搞定,说明对静态服务器的特性,http协议,浏览器行为,jdk的使用,设计模式的运用有更加深入的理解了。成熟的软件只是做的更加精细,考虑了更多的corner case而已。没有足够的基础,只能是在数十万代码中迷失方向什么也无所得。

祝好运。