Python 语法速览与实战清单

本文是对于 现代 Python 开发:语法基础与工程实践的总结,更多 Python 相关资料参考 Python 学习与实践资料索引;本文参考了 Python Crash Course – Cheat Sheetspysheeet等。本文仅包含笔者在日常工作中经常使用的,并且认为较为关键的知识点与语法,如果想要进一步学习 Python 相关内容或者对于机器学习与数据挖掘方向感兴趣,可以参考程序猿的数据科学与机器学习实战手册

基础语法

Python 是一门高阶、动态类型的多范式编程语言;定义 Python 文件的时候我们往往会先声明文件编码方式:

# 指定脚本调用方式
#!/usr/bin/env python
# 配置 utf-8 编码
# -*- coding: utf-8 -*-

# 配置其他编码
# -*- coding: <encoding-name> -*-

# Vim 中还可以使用如下方式
# vim:fileencoding=<encoding-name>

人生苦短,请用 Python,大量功能强大的语法糖的同时让很多时候 Python 代码看上去有点像伪代码。譬如我们用 Python 实现的简易的快排相较于 Java 会显得很短小精悍:

def quicksort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) / 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quicksort(left) + middle + quicksort(right)
    
print quicksort([3,6,8,10,1,2,1])
# Prints "[1, 1, 2, 3, 6, 8, 10]"

控制台交互

可以根据 __name__ 关键字来判断是否是直接使用 python 命令执行某个脚本,还是外部引用;Google 开源的 fire 也是不错的快速将某个类封装为命令行工具的框架:

import fire

class Calculator(object):
  """A simple calculator class."""

  def double(self, number):
    return 2 * number

if __name__ == '__main__':
  fire.Fire(Calculator)

# python calculator.py double 10  # 20
# python calculator.py double --number=15  # 30

Python 2 中 print 是表达式,而 Python 3 中 print 是函数;如果希望在 Python 2 中将 print 以函数方式使用,则需要自定义引入:

from __future__ import print_function

我们也可以使用 pprint 来美化控制台输出内容:

import pprint

stuff = ['spam', 'eggs', 'lumberjack', 'knights', 'ni']
pprint.pprint(stuff)

# 自定义参数
pp = pprint.PrettyPrinter(depth=6)
tup = ('spam', ('eggs', ('lumberjack', ('knights', ('ni', ('dead',('parrot', ('fresh fruit',))))))))
pp.pprint(tup)

模块

Python 中的模块(Module)即是 Python 源码文件,其可以导出类、函数与全局变量;当我们从某个模块导入变量时,函数名往往就是命名空间(Namespace)。而 Python 中的包(Package)则是模块的文件夹,往往由 __init__.py 指明某个文件夹为包:

# 文件目录
someDir/
    main.py
    siblingModule.py

# siblingModule.py

def siblingModuleFun():
    print('Hello from siblingModuleFun')
    
def siblingModuleFunTwo():
    print('Hello from siblingModuleFunTwo')

import siblingModule
import siblingModule as sibMod

sibMod.siblingModuleFun()

from siblingModule import siblingModuleFun
siblingModuleFun()

try:
    # Import 'someModuleA' that is only available in Windows
    import someModuleA
except ImportError:
    try:
        # Import 'someModuleB' that is only available in Linux
        import someModuleB
    except ImportError:

Package 可以为某个目录下所有的文件设置统一入口:

someDir/
    main.py
    subModules/
        __init__.py
        subA.py
        subSubModules/
            __init__.py
            subSubA.py

# subA.py

def subAFun():
    print('Hello from subAFun')
    
def subAFunTwo():
    print('Hello from subAFunTwo')

# subSubA.py

def subSubAFun():
    print('Hello from subSubAFun')
    
def subSubAFunTwo():
    print('Hello from subSubAFunTwo')

# __init__.py from subDir

# Adds 'subAFun()' and 'subAFunTwo()' to the 'subDir' namespace 
from .subA import *

# The following two import statement do the same thing, they add 'subSubAFun()' and 'subSubAFunTwo()' to the 'subDir' namespace. The first one assumes '__init__.py' is empty in 'subSubDir', and the second one, assumes '__init__.py' in 'subSubDir' contains 'from .subSubA import *'.

# Assumes '__init__.py' is empty in 'subSubDir'
# Adds 'subSubAFun()' and 'subSubAFunTwo()' to the 'subDir' namespace
from .subSubDir.subSubA import *

# Assumes '__init__.py' in 'subSubDir' has 'from .subSubA import *'
# Adds 'subSubAFun()' and 'subSubAFunTwo()' to the 'subDir' namespace
from .subSubDir import *
# __init__.py from subSubDir

# Adds 'subSubAFun()' and 'subSubAFunTwo()' to the 'subSubDir' namespace
from .subSubA import *

# main.py

import subDir

subDir.subAFun() # Hello from subAFun
subDir.subAFunTwo() # Hello from subAFunTwo
subDir.subSubAFun() # Hello from subSubAFun
subDir.subSubAFunTwo() # Hello from subSubAFunTwo

表达式与控制流

条件选择

Python 中使用 if、elif、else 来进行基础的条件选择操作:

if x < 0:
     x = 0
     print('Negative changed to zero')
 elif x == 0:
     print('Zero')
 else:
     print('More')

Python 同样支持 ternary conditional operator:

a if condition else b

也可以使用 Tuple 来实现类似的效果:

# test 需要返回 True 或者 False
(falseValue, trueValue)[test]

# 更安全的做法是进行强制判断
(falseValue, trueValue)[test == True]

# 或者使用 bool 类型转换函数
(falseValue, trueValue)[bool(<expression>)]

循环遍历

for-in 可以用来遍历数组与字典:

words = ['cat', 'window', 'defenestrate']

for w in words:
    print(w, len(w))

# 使用数组访问操作符,能够迅速地生成数组的副本
for w in words[:]:
    if len(w) > 6:
        words.insert(0, w)

# words -> ['defenestrate', 'cat', 'window', 'defenestrate']

如果我们希望使用数字序列进行遍历,可以使用 Python 内置的 range 函数:

a = ['Mary', 'had', 'a', 'little', 'lamb']

for i in range(len(a)):
    print(i, a[i])

基本数据类型

可以使用内建函数进行强制类型转换(Casting):

int(str)
float(str)
str(int)
str(float)

Number: 数值类型

x = 3
print type(x) # Prints "<type 'int'>"
print x       # Prints "3"
print x + 1   # Addition; prints "4"
print x - 1   # Subtraction; prints "2"
print x * 2   # Multiplication; prints "6"
print x ** 2  # Exponentiation; prints "9"
x += 1
print x  # Prints "4"
x *= 2
print x  # Prints "8"
y = 2.5
print type(y) # Prints "<type 'float'>"
print y, y + 1, y * 2, y ** 2 # Prints "2.5 3.5 5.0 6.25"

布尔类型

Python 提供了常见的逻辑操作符,不过需要注意的是 Python 中并没有使用 &&、|| 等,而是直接使用了英文单词。

t = True
f = False
print type(t) # Prints "<type 'bool'>"
print t and f # Logical AND; prints "False"
print t or f  # Logical OR; prints "True"
print not t   # Logical NOT; prints "False"
print t != f  # Logical XOR; prints "True" 

String: 字符串

Python 2 中支持 Ascii 码的 str() 类型,独立的 unicode() 类型,没有 byte 类型;而 Python 3 中默认的字符串为 utf-8 类型,并且包含了 byte 与 bytearray 两个字节类型:

type("Guido") # string type is str in python2
# <type 'str'>

# 使用 __future__ 中提供的模块来降级使用 Unicode
from __future__ import unicode_literals
type("Guido") # string type become unicode
# <type 'unicode'>

Python 字符串支持分片、模板字符串等常见操作:

var1 = 'Hello World!'
var2 = "Python Programming"

print "var1[0]: ", var1[0]
print "var2[1:5]: ", var2[1:5]
# var1[0]:  H
# var2[1:5]:  ytho

print "My name is %s and weight is %d kg!" % ('Zara', 21)
# My name is Zara and weight is 21 kg!
str[0:4]
len(str)

string.replace("-", " ")
",".join(list)
"hi {0}".format('j')
str.find(",")
str.index(",")   # same, but raises IndexError
str.count(",")
str.split(",")

str.lower()
str.upper()
str.title()

str.lstrip()
str.rstrip()
str.strip()

str.islower()
# 移除所有的特殊字符
re.sub('[^A-Za-z0-9]+', '', mystring) 

如果需要判断是否包含某个子字符串,或者搜索某个字符串的下标:

# in 操作符可以判断字符串
if "blah" not in somestring: 
    continue

# find 可以搜索下标
s = "This be a string"
if s.find("is") == -1:
    print "No 'is' here!"
else:
    print "Found 'is' in the string."

Regex: 正则表达式

import re

# 判断是否匹配
re.match(r'^[aeiou]', str)

# 以第二个参数指定的字符替换原字符串中内容
re.sub(r'^[aeiou]', '?', str)
re.sub(r'(xyz)', r'\1', str)

# 编译生成独立的正则表达式对象
expr = re.compile(r'^...$')
expr.match(...)
expr.sub(...)

下面列举了常见的表达式使用场景:

# 检测是否为 HTML 标签
re.search('<[^/>][^>]*>', '<a href="#label">')

# 常见的用户名密码
re.match('^[a-zA-Z0-9-_]{3,16}$', 'Foo') is not None
re.match('^\w|[-_]{3,16}$', 'Foo') is not None

# Email
re.match('^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$', 'hello.world@example.com')

# Url
exp = re.compile(r'''^(https?:\/\/)? # match http or https
                ([\da-z\.-]+)            # match domain
                \.([a-z\.]{2,6})         # match domain
                ([\/\w \.-]*)\/?$        # match api or file
                ''', re.X)
exp.match('www.google.com')

# IP 地址
exp = re.compile(r'''^(?:(?:25[0-5]
                     |2[0-4][0-9]
                     |[1]?[0-9][0-9]?)\.){3}
                     (?:25[0-5]
                     |2[0-4][0-9]
                     |[1]?[0-9][0-9]?)$''', re.X)
exp.match('192.168.1.1')

集合类型

List: 列表

Operation: 创建增删

list 是基础的序列类型:

l = []
l = list()

# 使用字符串的 split 方法,可以将字符串转化为列表
str.split(".")

# 如果需要将数组拼装为字符串,则可以使用 join 
list1 = ['1', '2', '3']
str1 = ''.join(list1)

# 如果是数值数组,则需要先进行转换
list1 = [1, 2, 3]
str1 = ''.join(str(e) for e in list1)

可以使用 append 与 extend 向数组中插入元素或者进行数组连接

x = [1, 2, 3]

x.append([4, 5]) # [1, 2, 3, [4, 5]]

x.extend([4, 5]) # [1, 2, 3, 4, 5],注意 extend 返回值为 None

可以使用 pop、slices、del、remove 等移除列表中元素:

myList = [10,20,30,40,50]

# 弹出第二个元素
myList.pop(1) # 20
# myList: myList.pop(1)

# 如果不加任何参数,则默认弹出最后一个元素
myList.pop()

# 使用 slices 来删除某个元素
a = [  1, 2, 3, 4, 5, 6 ]
index = 3 # Only Positive index
a = a[:index] + a[index+1 :]

# 根据下标删除元素
myList = [10,20,30,40,50]
rmovIndxNo = 3
del myList[rmovIndxNo] # myList: [10, 20, 30, 50]

# 使用 remove 方法,直接根据元素删除
letters = ["a", "b", "c", "d", "e"]
numbers.remove(numbers[1])
print(*letters) # used a * to make it unpack you don't have to

Iteration: 索引遍历

你可以使用基本的 for 循环来遍历数组中的元素,就像下面介个样纸:

animals = ['cat', 'dog', 'monkey']
for animal in animals:
    print animal
# Prints "cat", "dog", "monkey", each on its own line.

如果你在循环的同时也希望能够获取到当前元素下标,可以使用 enumerate 函数:

animals = ['cat', 'dog', 'monkey']
for idx, animal in enumerate(animals):
    print '#%d: %s' % (idx + 1, animal)
# Prints "#1: cat", "#2: dog", "#3: monkey", each on its own line

Python 也支持切片(Slices):

nums = range(5)    # range is a built-in function that creates a list of integers
print nums         # Prints "[0, 1, 2, 3, 4]"
print nums[2:4]    # Get a slice from index 2 to 4 (exclusive); prints "[2, 3]"
print nums[2:]     # Get a slice from index 2 to the end; prints "[2, 3, 4]"
print nums[:2]     # Get a slice from the start to index 2 (exclusive); prints "[0, 1]"
print nums[:]      # Get a slice of the whole list; prints ["0, 1, 2, 3, 4]"
print nums[:-1]    # Slice indices can be negative; prints ["0, 1, 2, 3]"
nums[2:4] = [8, 9] # Assign a new sublist to a slice
print nums         # Prints "[0, 1, 8, 9, 4]"

Comprehensions: 变换

Python 中同样可以使用 map、reduce、filter,map 用于变换数组:

# 使用 map 对数组中的每个元素计算平方
items = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, items))

# map 支持函数以数组方式连接使用
def multiply(x):
    return (x*x)
def add(x):
    return (x+x)

funcs = [multiply, add]
for i in range(5):
    value = list(map(lambda x: x(i), funcs))
    print(value)

reduce 用于进行归纳计算:

# reduce 将数组中的值进行归纳

from functools import reduce
product = reduce((lambda x, y: x * y), [1, 2, 3, 4])

# Output: 24

filter 则可以对数组进行过滤:

number_list = range(-5, 5)
less_than_zero = list(filter(lambda x: x < 0, number_list))
print(less_than_zero)

# Output: [-5, -4, -3, -2, -1]

字典类型

创建增删

d = {'cat': 'cute', 'dog': 'furry'}  # 创建新的字典
print d['cat']       # 字典不支持点(Dot)运算符取值

如果需要合并两个或者多个字典类型:

# python 3.5
z = {**x, **y}

# python 2.7
def merge_dicts(*dict_args):
    """
    Given any number of dicts, shallow copy and merge into a new dict,
    precedence goes to key value pairs in latter dicts.
    """
    result = {}
    for dictionary in dict_args:
        result.update(dictionary)
    return result

索引遍历

可以根据键来直接进行元素访问:

# Python 中对于访问不存在的键会抛出 KeyError 异常,需要先行判断或者使用 get
print 'cat' in d     # Check if a dictionary has a given key; prints "True"

# 如果直接使用 [] 来取值,需要先确定键的存在,否则会抛出异常
print d['monkey']  # KeyError: 'monkey' not a key of d

# 使用 get 函数则可以设置默认值
print d.get('monkey', 'N/A')  # Get an element with a default; prints "N/A"
print d.get('fish', 'N/A')    # Get an element with a default; prints "wet"


d.keys() # 使用 keys 方法可以获取所有的键

可以使用 for-in 来遍历数组:

# 遍历键
for key in d:

# 比前一种方式慢
for k in dict.keys(): ...

# 直接遍历值
for value in dict.itervalues(): ...

# Python 2.x 中遍历键值
for key, value in d.iteritems():

# Python 3.x 中遍历键值
for key, value in d.items():

其他序列类型

集合

# Same as {"a", "b","c"}
normal_set = set(["a", "b","c"])
 
# Adding an element to normal set is fine
normal_set.add("d")
 
print("Normal Set")
print(normal_set)
 
# A frozen set
frozen_set = frozenset(["e", "f", "g"])
 
print("Frozen Set")
print(frozen_set)
 
# Uncommenting below line would cause error as
# we are trying to add element to a frozen set
# frozen_set.add("h")

函数

函数定义

Python 中的函数使用 def 关键字进行定义,譬如:

def sign(x):
    if x > 0:
        return 'positive'
    elif x < 0:
        return 'negative'
    else:
        return 'zero'


for x in [-1, 0, 1]:
    print sign(x)
# Prints "negative", "zero", "positive"

Python 支持运行时创建动态函数,也即是所谓的 lambda 函数:

def f(x): return x**2

# 等价于
g = lambda x: x**2

参数

Option Arguments: 不定参数

def example(a, b=None, *args, **kwargs):
  print a, b
  print args
  print kwargs

example(1, "var", 2, 3, word="hello")
# 1 var
# (2, 3)
# {'word': 'hello'}

a_tuple = (1, 2, 3, 4, 5)
a_dict = {"1":1, "2":2, "3":3}
example(1, "var", *a_tuple, **a_dict)
# 1 var
# (1, 2, 3, 4, 5)
# {'1': 1, '2': 2, '3': 3}

生成器

def simple_generator_function():
    yield 1
    yield 2
    yield 3

for value in simple_generator_function():
    print(value)

# 输出结果
# 1
# 2
# 3
our_generator = simple_generator_function()
next(our_generator)
# 1
next(our_generator)
# 2
next(our_generator)
#3

# 生成器典型的使用场景譬如无限数组的迭代
def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1

装饰器

装饰器是非常有用的设计模式:

# 简单装饰器

from functools import wraps
def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('wrap function')
        return func(*args, **kwargs)
    return wrapper

@decorator
def example(*a, **kw):
    pass

example.__name__  # attr of function preserve
# 'example'
# Decorator 

# 带输入值的装饰器

from functools import wraps
def decorator_with_argument(val):
  def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
      print "Val is {0}".format(val)
      return func(*args, **kwargs)
    return wrapper
  return decorator

@decorator_with_argument(10)
def example():
  print "This is example function."

example()
# Val is 10
# This is example function.

# 等价于

def example():
  print "This is example function."

example = decorator_with_argument(10)(example)
example()
# Val is 10
# This is example function.

类与对象

类定义

Python 中对于类的定义也很直接:

class Greeter(object):
    
    # Constructor
    def __init__(self, name):
        self.name = name  # Create an instance variable
        
    # Instance method
    def greet(self, loud=False):
        if loud:
            print 'HELLO, %s!' % self.name.upper()
        else:
            print 'Hello, %s' % self.name
        
g = Greeter('Fred')  # Construct an instance of the Greeter class
g.greet()            # Call an instance method; prints "Hello, Fred"
g.greet(loud=True)   # Call an instance method; prints "HELLO, FRED!"
# isinstance 方法用于判断某个对象是否源自某个类
ex = 10
isinstance(ex,int)

Managed Attributes: 受控属性

# property、setter、deleter 可以用于复写点方法

class Example(object):
    def __init__(self, value):
       self._val = value
    @property
    def val(self):
        return self._val
    @val.setter
    def val(self, value):
        if not isintance(value, int):
            raise TypeError("Expected int")
        self._val = value
    @val.deleter
    def val(self):
        del self._val
    @property
    def square3(self):
        return 2**3

ex = Example(123)
ex.val = "str"
# Traceback (most recent call last):
#   File "", line 1, in
#   File "test.py", line 12, in val
#     raise TypeError("Expected int")
# TypeError: Expected int

类方法与静态方法

class example(object):
  @classmethod
  def clsmethod(cls):
    print "I am classmethod"
  @staticmethod
  def stmethod():
    print "I am staticmethod"
  def instmethod(self):
    print "I am instancemethod"

ex = example()
ex.clsmethod()
# I am classmethod
ex.stmethod()
# I am staticmethod
ex.instmethod()
# I am instancemethod
example.clsmethod()
# I am classmethod
example.stmethod()
# I am staticmethod
example.instmethod()
# Traceback (most recent call last):
#   File "", line 1, in
# TypeError: unbound method instmethod() ...

对象

实例化

属性操作

Python 中对象的属性不同于字典键,可以使用点运算符取值,直接使用 in 判断会存在问题:

class A(object):
    @property
    def prop(self):
        return 3

a = A()
print "'prop' in a.__dict__ =", 'prop' in a.__dict__
print "hasattr(a, 'prop') =", hasattr(a, 'prop')
print "a.prop =", a.prop

# 'prop' in a.__dict__ = False
# hasattr(a, 'prop') = True
# a.prop = 3

建议使用 hasattr、getattr、setattr 这种方式对于对象属性进行操作:

class Example(object):
  def __init__(self):
    self.name = "ex"
  def printex(self):
    print "This is an example"


# Check object has attributes
# hasattr(obj, 'attr')
ex = Example()
hasattr(ex,"name")
# True
hasattr(ex,"printex")
# True
hasattr(ex,"print")
# False

# Get object attribute
# getattr(obj, 'attr')
getattr(ex,'name')
# 'ex'

# Set object attribute
# setattr(obj, 'attr', value)
setattr(ex,'name','example')
ex.name
# 'example'

异常与测试

异常处理

Context Manager – with

with 常用于打开或者关闭某些资源:

host = 'localhost'
port = 5566
with Socket(host, port) as s:
    while True:
        conn, addr = s.accept()
        msg = conn.recv(1024)
        print msg
        conn.send(msg)
        conn.close()

单元测试

from __future__ import print_function

import unittest

def fib(n):
    return 1 if n<=2 else fib(n-1)+fib(n-2)

def setUpModule():
        print("setup module")
def tearDownModule():
        print("teardown module")

class TestFib(unittest.TestCase):

    def setUp(self):
        print("setUp")
        self.n = 10
    def tearDown(self):
        print("tearDown")
        del self.n
    @classmethod
    def setUpClass(cls):
        print("setUpClass")
    @classmethod
    def tearDownClass(cls):
        print("tearDownClass")
    def test_fib_assert_equal(self):
        self.assertEqual(fib(self.n), 55)
    def test_fib_assert_true(self):
        self.assertTrue(fib(self.n) == 55)

if __name__ == "__main__":
    unittest.main()

存储

文件读写

路径处理

Python 内置的 __file__ 关键字会指向当前文件的相对路径,可以根据它来构造绝对路径,或者索引其他文件:

# 获取当前文件的相对目录
dir = os.path.dirname(__file__) # src\app

## once you're at the directory level you want, with the desired directory as the final path node:
dirname1 = os.path.basename(dir) 
dirname2 = os.path.split(dir)[1] ## if you look at the documentation, this is exactly what os.path.basename does.

# 获取当前代码文件的绝对路径,abspath 会自动根据相对路径与当前工作空间进行路径补全
os.path.abspath(os.path.dirname(__file__)) # D:\WorkSpace\OWS\tool\ui-tool-svn\python\src\app

# 获取当前文件的真实路径
os.path.dirname(os.path.realpath(__file__)) # D:\WorkSpace\OWS\tool\ui-tool-svn\python\src\app

# 获取当前执行路径
os.getcwd()

可以使用 listdir、walk、glob 模块来进行文件枚举与检索:

# 仅列举所有的文件
from os import listdir
from os.path import isfile, join
onlyfiles = [f for f in listdir(mypath) if isfile(join(mypath, f))]

# 使用 walk 递归搜索
from os import walk

f = []
for (dirpath, dirnames, filenames) in walk(mypath):
    f.extend(filenames)
    break

# 使用 glob 进行复杂模式匹配
import glob
print(glob.glob("/home/adam/*.txt"))
# ['/home/adam/file1.txt', '/home/adam/file2.txt', .... ]

简单文件读写

# 可以根据文件是否存在选择写入模式
mode = 'a' if os.path.exists(writepath) else 'w'

# 使用 with 方法能够自动处理异常
with open("file.dat",mode) as f:
    f.write(...)
    ...
    # 操作完毕之后记得关闭文件
    f.close()

# 读取文件内容
message = f.read()

复杂格式文件

JSON

import json

# Writing JSON data
with open('data.json', 'w') as f:
     json.dump(data, f)

# Reading data back
with open('data.json', 'r') as f:
     data = json.load(f)

XML

我们可以使用 lxml 来解析与处理 XML 文件,本部分即对其常用操作进行介绍。lxml 支持从字符串或者文件中创建 Element 对象:

from lxml import etree

# 可以从字符串开始构造
xml = '<a xmlns="test"><b xmlns="test"/></a>'
root = etree.fromstring(xml)
etree.tostring(root)
# b'<a xmlns="test"><b xmlns="test"/></a>'

# 也可以从某个文件开始构造
tree = etree.parse("doc/test.xml")

# 或者指定某个 baseURL
root = etree.fromstring(xml, base_url="http://where.it/is/from.xml")

其提供了迭代器以对所有元素进行遍历:

# 遍历所有的节点
for tag in tree.iter():
    if not len(tag):
        print tag.keys() # 获取所有自定义属性
        print (tag.tag, tag.text) # text 即文本子元素值

# 获取 XPath
for e in root.iter():
    print tree.getpath(e)

lxml 支持以 XPath 查找元素,不过需要注意的是,XPath 查找的结果是数组,并且在包含命名空间的情况下,需要指定命名空间:

root.xpath('//page/text/text()',ns={prefix:url})

# 可以使用 getparent 递归查找父元素
el.getparent()

lxml 提供了 insert、append 等方法进行元素操作:

# append 方法默认追加到尾部
st = etree.Element("state", name="New Mexico")
co = etree.Element("county", name="Socorro")
st.append(co)

# insert 方法可以指定位置
node.insert(0, newKid)

Excel

可以使用 [xlrd]() 来读取 Excel 文件,使用 xlsxwriter 来写入与操作 Excel 文件。

# 读取某个 Cell 的原始值
sh.cell(rx, col).value
# 创建新的文件
workbook = xlsxwriter.Workbook(outputFile)
worksheet = workbook.add_worksheet()

# 设置从第 0 行开始写入
row = 0

# 遍历二维数组,并且将其写入到 Excel 中
for rowData in array:
    for col, data in enumerate(rowData):
        worksheet.write(row, col, data)
    row = row + 1

workbook.close()

文件系统

对于高级的文件操作,我们可以使用 Python 内置的 shutil

# 递归删除 appName 下面的所有的文件夹
shutil.rmtree(appName)

网络交互

Requests

Requests 是优雅而易用的 Python 网络请求库:

import requests

r = requests.get('https://api.github.com/events')
r = requests.get('https://api.github.com/user', auth=('user', 'pass'))

r.status_code
# 200
r.headers['content-type']
# 'application/json; charset=utf8'
r.encoding
# 'utf-8'
r.text
# u'{"type":"User"...'
r.json()
# {u'private_gists': 419, u'total_private_repos': 77, ...}

r = requests.put('http://httpbin.org/put', data = {'key':'value'})
r = requests.delete('http://httpbin.org/delete')
r = requests.head('http://httpbin.org/get')
r = requests.options('http://httpbin.org/get')

数据存储

MySQL

import pymysql.cursors

# Connect to the database
connection = pymysql.connect(host='localhost',
                             user='user',
                             password='passwd',
                             db='db',
                             charset='utf8mb4',
                             cursorclass=pymysql.cursors.DictCursor)

try:
    with connection.cursor() as cursor:
        # Create a new record
        sql = "INSERT INTO `users` (`email`, `password`) VALUES (%s, %s)"
        cursor.execute(sql, ('webmaster@python.org', 'very-secret'))

    # connection is not autocommit by default. So you must commit to save
    # your changes.
    connection.commit()

    with connection.cursor() as cursor:
        # Read a single record
        sql = "SELECT `id`, `password` FROM `users` WHERE `email`=%s"
        cursor.execute(sql, ('webmaster@python.org',))
        result = cursor.fetchone()
        print(result)
finally:
    connection.close() 
「真诚赞赏,手留余香」

初学机器学习的你,是否掌握了这样的Linux技巧?

随着软件系统的不断发展,今天,不同的操作系统对应着不同的适用人群:Windows 面向办公室和商用,Mac 面向创意人群,而 Linux 面向软件开发者。对于操作系统提供商而言,这种市场分割大幅度简化了产品技术需求、用户体验和产品方向上的投入。然而,这也加剧了兼容性问题,让不同业务进入了狭窄、互不相容的领域:商务人士无法对创意提供洞察力,而开发者也无法深入到商务决策中去。

在现实中,知识和技能是流动的,跨越多个学科和领域。与其说「你只能擅长一件事」的理念是迈向精通的路线图,还不如说一种过早优化的方法。一旦从社会中采样大量的任务,你就只能知道你擅长什么,也许你还发现自己擅长它们中的很多。

对于现代的业务分析师,弥补业务与软件之间的鸿沟尤其重要。业务分析必须是「双重平台」,能够利用仅在 Linux(或 OS X)上可用的命令行工具,但是仍然受益于 Windows 的 Microsoft Office。可以理解的是,Linux 会使具有商学学位的人感到恐惧。幸运的是,正如大多数事情一样,你只需 20% 的任务即可完成 80% 的工作。下面是我的 20%。

业务分析是基于数据的,而机器学习正是强大的数据分析工具。我们利用机器学习模型分析数据最好的环境却恰恰是 Linux 系统,这不仅是因为它支持广泛的 Python 机器学习库,同时在于环境配置与管理的简单明了。因此,本文将为机器学习读者梳理 Linux 系统的基本特性与命令。

为什么机器学习分析师需要了解 Linux

由于其开源的底层,Linux 从不断从数以万计的开发者贡献中受益。他们构建的程序和工具不仅使其工作更简单,也简化了跟随他们的编程人员的工作。结果,开源开发带来了一种网络效应:在平台上构建工具的开发者越多,能够利用这些工具立刻编写其程序的其他开发者就越多。

结果就是 Linux 中编写的 Linux 程序和实用工具(统称为软件)的扩展套件——其中很多从未用于 Windows。一个示例是被称作 git 的流行的版本控制系统(VCS)。开发者本可以编写这一在 Windows 工作的软件,但是却没有。他们让其在 Linux 命令行上工作,因为生态系统已经提供全部所需的工具。

具体来说,Windows 上的开发有两个主要问题:

1. 基本任务,比如文件解析、工作调度和文本搜索比运行命令行工具更为重要。

2. 编程语言(比如 Python、C++)及其相关代码库会引发错误,因为它们期望特定的 Linux 参数或文件系统定位。

这意味着若想在 Windows 上进行开发,我们需要花费更多的时间来重写 Linux 中已有的基本工具,并排除操作系统兼容性错误。这并不令人意外——Windows 生态系统当初并没有考虑软件开发设计的需求。

借助这个 Linux 开发案例,让我们从最基本的开始。

Linux 的基本单元:「shell」

「shell」(也被称为终端、控制台或命令行)是一个基于文本的用户界面,通过它把命令发送给机器。在 Linux 中,shell 的默认语言是 bash。与主要在 Windows 内部进行点击操作的 Windows 用户不同,Linux 开发者坚持使用键盘把命令输入到 shell。对于那些没有编程背景的人来说,这种转变一开始也许会不自然,但是在 Linux 中开发的好处很容易超过最初的学习投资。

学习几个重要的概念

和成熟的编程语言相比,bash 只需要学习几个主要的概念。这一步完成之后,之后 bash 的学习就只剩下记忆了。更清楚地说就是:要学好 bash,只需要记住 20—30 个命令(command)以及其中最常用的参数(argument)就可以了。

对于非开发者而言,Linux 很令人费解,因为开发者似乎能随意且不费力地使用深奥的终端命令。其实是因为他们只记住了少量的命令—对于更复杂的问题,他们(和所有普通人一样)也需要谷歌一下。

以下就是 bash 中的主要概念。

命令语法

bash 中的命令是区分大小写的,且遵循 {命令}{参数} 的语法结构。

例如,在『grep-inr』中,grep 是命令(搜索文本的一个字符串),-inr 是标记(flag)或参数(随 grep 默认运行而变化)。理解这个命令的唯一方法是使用谷歌搜索,或输入『man grep』命令。我推荐同时学习命令和其中最常用的参数,否则单独学习每一个标记的作用是很费力的。

目录相对地址

当前目录:.

上一级目录的上一级目录:..

用户的主目录:~

文件的系统根目录:/

例如,为了从当前目录换到上一级目录,需要输入:「cd..」。类似地,为了复制位于「/path/to/file.txt」文件到上一级目录中,需要输入「cp /path/to/file.txt.」(请注意命令末尾的点)。这些例子中使用的都是相对路径,可以使用绝对路径替换。

标准输入(STDIN)/标准输出(STDOUT)

任何输入和提交(通过键入 ENTER)到窗口的命令都被称为标准输入(standard input,STDIN)。

任何程序打印(print)到终端的东西(例如,一份文件中的文本)都被称为标准输出(standard output,STDOUT)。

管道(PIPING)

1 |

一种管道,其左方是一个命令的 STNOUT,将作为管道右方的另一个命令的 STDIN。

例如:echo ‘test text’ | wc -l

2 >

大于号,作用是取一个命令 STDOUT 位于左方,并将其写入/覆写(overwrite)入右方的一个新文件。

例如:ls > tmp.txt

3 >>

两个大于号,作用是取一个命令 STDOUT 位于左方,并将其追加到右方的一个新的或现有文件中。

例如:date >> tmp.txt

通配符(WILDCARDS)

这类似于 SQL 中的% 符号,例如,使用「WHERE first_name LIKE 『John%』」搜索所有以 John 起始的名字。

在 bash 中,相应的命令是「John*」。如果想列出一个文件夹中所有以「.json」结尾的文件,可以输入:「ls *.json」。

TAB 键自动完成

如果我们输入一个命令并按下 TAB 键,那么 Bash 将自动完成该命令。但是,我们也应该使用一些如 zsh 或 fish 工具来自动完成,因为我们很难记住各种命令及它们的参数。更准确地说,这些工具会基于我们的命令行历史自动完成命令语句。

退出

有时候我们会卡在一些程序中并不知道如何退出它们。这在 Linux 新手中是很常见的问题,这也会大大损害新手的积极性。一般来说,退出命令会和字母「q」有一些关系,所以记住以下的退出命令或快捷键就十分有用了。

  • Bash

CTRL+c

q

exit

  • Python

quit()

CTRL+d

  • Nano: CTRL+x
  • Vim: <Esc> :q!

常用 Bash 命令

以下是在 Linux 中最常用到的指令,在使用新系统进行开发时,记住这些指令对于快速上手非常重要。

  • cd {directory}:转换当前目录
  • ls -lha:列出目录文件(详细信息)
  • vim or nano:命令行编辑器
  • touch {file}:创建一个新的空文件
  • cp -R {original_name} {new_name}:复制一个文件或目录(包含内部所有文件)
  • mv {original_name} {new_name}:移动或重命名文件
  • rm {file}:删除文件
  • rm -rf {file/folder}:永久删除文件或文件夹(小心使用)
  • pwb:打印当前工作目录
  • cat or less or tail or head -n10 {file}:文件的标准输出内容
  • mkdir {directory}:创建一个空的目录
  • grep -inr {string}:在当前目录或子目录的文件中搜索一个字符串
  • column -s, -t <delimited_file>:在 columnar 格式中展示逗号分隔文件
  • ssh {username}@{hostname}:连接到远程机器中
  • tree -LhaC 3:向下展示三级目录结构(带有文件大小信息和隐藏目录信息)
  • htop (or top):任务管理器
  • pip install –user {pip_package}:Python 安装包管理器,安装包到~/.local/bin 目录下
  • pushd . ; popd ; dirs; cd -:在堆栈上 push/pop/view 一个目录,并变回最后一个目录
  • sed -i “s/{find}/{replace}/g” {file}:替代文件中的一个字符串
  • find . -type f -name ‘*.txt’ -exec sed -i “s/{find}/{replace}/g” {} \;:替换当前目录和子目录下后缀名为.txt 文件的一个字符串
  • tmux new -s session, tmux attach -t session:创建另一个终端会话界面而不创建新的窗口 [高级命令]
  • wget {link}:下载一个网页或网页资源
  • curl -X POST -d “{key: value}” http://www.google.com:发送一个 HTTP 请求到网站服务器
  • find <directory>:递归地列出所有目录和其子目录的内容

高级 & 不常用的指令

保留一个有用命令列表以备不需也是非常必要的,即使这些情况不常发生(如某个进程阻塞了几个网络端口)。以下我们将列出几个不常用命令:

  • lsof -i :8080:列出打开文件的描述符(-i 是网络接口的标记)
  • netstat | head -n20:列出当前打开的 Internet/UNIX 接口(socket)以及相关信息
  • dstat -a:输出当前硬盘、网络、CPU 活动等信息
  • nslookup <IP address>:找到远程 IP 地址的主机名
  • strace -f -e <syscall> <cmd>:跟踪程序的系统调用(-e 标记用于过滤某些系统调用)
  • ps aux | head -n20:输出目前活动的进程
  • file <file>:检查文件类型(例如可执行文件、二进制文件、ASCII 文本文件)
  • uname -a:内核信息
  • lsb_release -a:系统信息
  • hostname:检视你的机器的主机名(即其他电脑可以搜索到的名称)
  • pstree:可视化分支进程
  • time <cmd>:执行一个命令并报告用时
  • CTRL + z ; bg; jobs; fg:从当前 tty 中传递一个进程到后台再返回前台
  • cat file.txt | xargs -n1 | sort | uniq -c:统计文件中的独特字(unique words)数量
  • wc -l <file>:计算文件的行数
  • du -ha:在磁盘上显示目录及其内容的大小
  • zcat <file.gz>:显示压缩文本文件的内容
  • scp <user@remote_host> <local_path>:将文件从远端复制到本地服务器,或反过来
  • man {command}:为一个命令显示 manual(说明文档),但是通常这样不如谷歌搜索好用

原文链接:http://alexpetralia.com/posts/2017/6/26/learning-linux-bash-to-get-things-done

后端服务性能压测实践

背景

最近大半年内有过两次负责性能压测的一些工作。一件事情做了一次可能还无法总结出一些东西,两次过后还是能发现一些共性问题,所以总结下性能压测的一般性实践。但是问题肯定不止这些,还有更多深层次的问题等着发现,等我们遇到了在逐个解决再来总结分享。

做性能压测的原因就不多说了,一般两个时间点是必须要做的,大促前、新系统上线。压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数。

从整个行业来看,抛开一些大厂不说,全自动化的性能压测环境还是比较少的,要想建设好一套全自动化的性能压测环境起码涉及到几个问题,CI\CD、独立、隔离的压测环境,自动化压测工具、日常压测性能报警、性能报表分析、排查/解决性能问题流程等等。这样才能将性能压测常规化,一旦不是常规化性能压测,就会有代码、中间件配置滞后于生产环境的问题。时间一长,就等于要重新开始搭建、排查压测环境。

如果性能压测的环境是全自动化的,那么就可以把性能压测工作常规化变成研发过程中的一个例行的事项,执行起来效率就会非常高,压测的时候也会比较轻松,好处也是比较明显的。

但是大多数的时候我们还是需要从零开始进行性能压测的工作。毕竟搭建这样一套环境给企业带来的成本也是巨大的。性能压测对环境敏感,必须划分独立的部署、隔离单元,才能在后续的常规压测流程中直观的阅读压测报告。

题外话,如果有了自动化的压测环境,也还是需要去了解下整个压测环境的基本架构,毕竟压测环境不是真实的生产环境,有些问题我们需要知道是正常的还是不正常的。

环境检测

当我们需要进行性能压测时首先要面对的问题就是环境问题,环境问题包含了常见的几个点:

1.机器问题(实体机还是虚拟机、CPU、内存、网络适配器进出口带宽、硬盘大小,硬盘是否 SSD、内核基本参数配置)

2.网络问题(是否有跨网段问题、网段是否隔离、如果有跨网段机器,是否能访问、跨网段是否有带宽限速)

3.中间件问题(程序里所有依赖的中间件是否有部署,中间件的配置是否初始化、中间件 cluster 结构什么样、这些中间件是否都进行过性能压测、压测的纬度是什么,是 benchmark 还是针对特定业务场景的压测)

这些环境问题第一次排查的时候会有点累,但是掌握了一些方法、工具、流程之后剩下的也就是例行的事情,只不过人工参与的工作多点。

上面的问题里,有些问题查看是比较简单的,这里就不介绍了,比如机器的基本配置等。有些配置只需要你推动下,走下相关流程回头验收下,比如网段隔离等,也还是比较简单的。

比较说不清楚的是中间件问题,看上去都是能用的,但是就是压不上去,这时候就需要你自己去进行简单的压测,比如 db 的单表插入、cache 的并发读取、mq 的落地写入等。这时候就涉及到一个问题,你需要对这些中间件都有一定深度的了解,要知道内在的运行机制,要不然出现异常情况排查起来确实很困难。

其实没有人能熟悉市面上所有的中间件,每一个中间件都很复杂,我们也不可能掌握一个中间件的所有点,但是常用的一些我们是需要掌握的,至少知道个大概的内部结构,可以顺藤摸瓜的排查问题。

但是事实上总有你不熟悉的,这个时候求助下大家的力量互相探讨再自己摸索找点资料,我们没遇到过也许别人遇到过,学技术其实就是这么个过程。

压力机及压力工具检测

既然做性能压测就需要先对压测机、压力工具先进行了解,压测工具我们主要有 locustjmeterab,前两者主要是压测同事进行准出验收测试使用的。

后两者主要是用来提交压测前的自检使用,就是开发自己用来检查和排错使用的。这里需要强调下 ab 其实是做基准测试的,不同于 jmeter 的作用。

需要知道压力机是否和被压测机器服务器在一个网段,且网段之间没有任何带宽限制。压力机的压测工具配置是否有瓶颈,一般如果是 jmeter 的话需要检查 java 的一些基本配置。

但是一般如果压力机是固定不变的,一直在使用的,那么基本不会有什么问题,因为压力机压测同事一直维护者,反而是自己使用的压测工具的参数要做好配置和检测。

jmeter 压测的时候,如果压测时间过长,记得关掉 监听器->图形结果 面板,因为那个渲染如果时间太长基本会假死,误以为会是内存的问题,其实是渲染问题。

在开发做基准压测的时候有一个问题就是办公网络与压测服务器的网络之间的带宽问题,压力过大会导致办公网络出现问题。所以需要错开时间段。

大致梳理好后,我们需要通过一些工具来查看下基本配置是否正常。比如,ethtool 网络适配器信息、nload 流量情况等等,当然还有很多其他优秀的工具用来查看各项配置,这里就不罗列了。

使用 ethtool 查看网络适配器信息前需要先确定当前机器有几个网络适配器,最好的办法是使用 ifconfig找到你正在使用的网络适配器。

排除 127.0.0.1 的适配器外,还有三个适配器信息,只有第一个 bond0 才是我们正在使用的,然后使用 ethtool 查看当前 bond0 的详细适配器信息。重点关注下 speed 域,它表示当前网络适配器的带宽。

虽然网络适配器可能配置的没有问题,但是整个网络是否没问题还需要咨询相关的运维同事进行排查下,中间还可能存在限速问题。

要确定网络带宽确实没有问题,我们还需要一个实时的监控网络流量工具,这里我们使用nload来监控下进出口流量问题。

这个工具还是很不错的,尤其是在压测的过程中可以观察流量的进出口情况,尤其是排查一些间隙抖动情况。

如果发现进口流量一直很正常,出口流量下来了有可能系统对外调用再放慢,有可能是下游调用 block,但是 request 线程池还未跑满,也有可能内部是纯 asyncrequest 线程根本不会跑满,也有可能是压测工具本身的压力问题等等。但是我们至少知道是自己的系统对外调用这个边界出了问题。

Linux openfiles limit 设置

工作环境中,一般情况下 linux 打开文件句柄数上限是不需要我们设置的,这些初始化的值运维同事一般是设置过的,而且是符合运维统一标准的。但是有时候关于最大连接数设置还要根据后端系统的使用场景来决定。

以防万一我们还是需要自己检查下是否符合当前系统的压测要求。

Linux 中一切都是文件,socket 也是文件,所以需要查看下当前机器对于文件句柄打开的限制,查看 ulimit -aopen files 域,也可以直接查看ulimit -n

如果觉得配置的参数需要调整,可以通过编辑 /etc/security/limits.conf 配置文件。

排查周边依赖

要想对一个服务进行压测,就需要对这个服务周边依赖进行一个排查,有可能你所依赖的服务不一定具备压测条件。并不是每个系统的压测都在一个时间段内,所以你在压测的时候别人的服务也许并不需要压测等等。

还有类似中间件的问题,比如,如果我们依赖中间件 cache ,那么是否有本地一级 cache ,如果有的话也许对压测环境的中间件 cache 依赖不是太大。如果我们依赖中间件 mq ,是不是在业务上可以断开对 mq的依赖,因为我们毕竟不是对 mq 进行压测。还有我们所依赖服务也不关心我们的压测波动。

整理出来之后最好能画个草图,再重新 git branch -b 重新拉一个性能压测的 branch 出来根据草图进行调整代码依赖。然后压测的时候观察流量和数据的走向,是否符合我们梳理之后的路线。

空接口压测检测

为了快速验证压测服务一个简单的办法,就是通过压测一个空接口,查看下整个网络是否通畅,各个参数是否大体上正常。

一般在任何一个后端服务中,都有类似 health_checkendpoint,方便起见可以直接找一个没有任何下游依赖的接口进行压测,这类接口主要是为了验证服务器的 onlineoffline 状态。

如果当前服务没有类似 health_check 新建一个空接口也可以,而且实践证明,一个服务在生产环境非常需要这么一个接口,必要情况下可以帮助来排查调用链路问题。

《发布!软件的设计与部署》Jolt 大奖图书 第17章 透明性 介绍了架构的透明性设计作用。

聚合报告中 throughput 计算

我们在用 jmeter 进行压测的时候关于 聚合报告 中的 throughput 理解需要统一下。

正常情况下在使用 jmeter 压测的时候会仔细观察 throughput 这一列的变化情况,但是没有搞清楚 thourghput 的计算原理的时候就会误以为是 tps/qps 下来了,其实有时候是整个远程服务器根本就没有 response 了。

throughput=samples/压测时间

throughput(吞吐量) 是单位时间内的请求处理数,一般是按 second 计算,如果是压测 write 类型的接口,那么就是 tps 指标。如果压测 read 类型的接口,那么就是 qps 指标。这两种类型的指标是完全不一样的,我们不能搞混淆了。

200(throughput) tps=1000(write)/5(s)1000(throughput) qps=2000(read)/2(s)

当我们发现 throughput 逐渐下来的时候要考虑一个时间的纬度。

也就是说我们的服务有可能已经不响应了,但是随着压测时间的积累,整个吞吐量的计算自然就在缓慢下滑,像这种刺尖问题是发现不了的。

这一点用ui版本的 jmeter 尤其明显,因为它的表现方式就是在欢欢放慢。用 Linux 版本的 jmeter 还好点,因为它的输出打印是隔断时间才打印。

关于这个点没有搞清楚非常影响我们对性能压测的结果判断。所以我们在压测的时候一定要有监控报表,才能知道在整个压测过程中服务器的各项指标是否出现过异常情况。

大多数的时候我们还会使用 apache ab 做下基本的压测,主要是用来与 jmeter 对比下,两个工具压测的结果是否相差不大,主要用来纠偏一些性能虚高问题。

apache abjmeter 各有侧重,ab 可以按固定请求数来压,jmeter 可以按时间来压,最后计算的时候需要注意两者区别。ab 好像是没有请求错误提示和中断的,jmeter 是有错误提示,还有各个纬度断言设置。

我们在使用压测工具的时候,大致了解下工具的一些原理有助于准确的使用这款工具。

压测及性能排查方法

在文章的前面部分讲到了 排查周边依赖 的环境检查步骤。其实要想顺利的进行压测,这一步是必须要有的。经过这一步分析我们会有一个基本的 系统依赖 roadmap

基于这份 系统依赖 roadmap 我们将进行性能压测和问题定位及性能优化。

合理的系统架构应该是上层依赖下层,在没有确定下游系统性能的情况下,是没办法确定上游系统性能的瓶颈在哪里。

所以压测的顺序应该尽可能的从下往上依次进行,这样可以避免无意义的排查由于下游吞吐量不够带来的性能问题。越是下游系统性能要求越高,因为上游系统的性能瓶颈直接依赖下游系统。

比如,商品系统的 v1/product/{productid} 前台接口,吞吐量为 qps 8000,那么所有依赖这个接口的上游服务在这个代码路径上最高吞吐量瓶颈就是 8000 ,代码路径不管是 tps 还是 qps 都是一样的瓶颈。

上层服务可以使用 async方式来提高 request 并发量,但是无法提高代码路径在 v1/product/{productid} 业务上的吞吐量。

我们不能将并发和吞吐量搞混淆了,系统能扛住多少并发不代表吞吐量就很高。可以有很多方式来提高并发量,threadpool 提高线程池大小 、socket 类c10k 、nio事件驱动,诸如此类方法。

关注各纬度 log

当在压测的过程中定位性能问题的性价比较高的方法就是请求处理的log,请求处理时长log,对外接口调用时长log,这一般能定位大部分比较明显的问题。当我们用到了一些中间件的时候都会输出相应的执行log。

如下所示,在我们所使用的开发框架中支持了很多纬度的执行log,这在排查问题的时候就会非常方便。

slow.log 类型的慢日志还是非常有必要记录下来的,这不仅在压测的时候需要,在生产上我们也是非常需要。

如果我们使用了各种中间件,那就需要输出各种中间件的处理日志,mq.logcache.logsearch.log 诸如此类。

除了这些 log 之外,我们还需要重点关注运行时的 gc log

我们主要使用 Java 平台,在压测的时候关注 gc log 是正常的事。哪怕不是 Java 程序,类似基于 vm 的语言都需要关注 gc log 。根据 jvm gcer 配置的不同,输出的日志也不太一样。

一般电商类的业务,以响应为优先时 gc 主要是使用 cms+prenew ,关注 full gc 频次,关注 cms 初始标记并发标记重新标记并发清除 各个阶段执行时间, gc 执行的 real timepernew 执行时的内存回收大小等 。

java gc 比较复杂涉及到的东西也非常多,对 gc log 的解读也需要配合当前的内存各个代的大小及一系列 gc 的相关配置不同而不同。

《Java性能优化权威指南》 java之父gosling推荐,可以长期研究和学习。

Linux 常规命令

在压测的过程中为了能观察到系统的各项资源消耗情况我们需要借助各种工具来查看,主要包括网络、内存、处理器、流量。

netstat

主要是用来查看各种网络相关信息。

比如,在压测的过程中,通过 netstat wc 看下 tcp 连接数是否和服务器 threadpool 设置的匹配。

netstat -tnlp | grep ip | wc -l

如果我们服务器的 threadpool 设置的是50,那么可以看到 tcp 连接数应该是50才对。然后再通过统计 jstack 服务器的 request runing 状态的线程数是不是>=50。

request 线程数的描述信息可能根据使用的 nio 框架的不同而不同。

还有使用频率最高的查看系统启动的端口状态、tcp 连接状态是 establelished 还是 listen 状态。

netstat -tnlp

再配合 ps 命令查看系统启动的状态。这一般用来确定程序是否真的启动了,如果启动了是不是 listen 的端口与配置中指定的端口不一致。

ps aux | grep ecm-placeorder

netstat 命令很强大有很多功能,如果我们需要查看命令的其他功能,可以使用man netstat 翻看帮助文档。

vmstat

主要用来监控虚拟处理器的运行队列统计信息。

vmstat 1

在压测的时候可以每隔 1s2s 打印一次,可以查看处理器负载是不是过高。procsr 子列就是当前处理器的处理队列,如果这个值超高当前 cpu core 数那么处理器负载将过高。可以和下面将介绍的 top 命令搭配着监控。

同时此命令可以在处理器过高的时候,查看内存是否够用是否出现大量的内存交换,换入换出的量多少 swap si 换入 swap so 换出。是否有非常高的上下文切换 system cs 每秒切换的次数,system us 用户态运行时间是否很少。是否有非常高的 io wait 等等。

关于这个命令网上已经有很多优秀的文章讲解,这里就不浪费时间重复了。同样可以使用 man vmstat 命令查看各种用法。

mpstat

主要用来监控多处理器统计信息

mpstat -P ALL 1

我这是一个 32 core 的压测服务器,通过 mpstat 可以监控每一个虚拟处理器的负载情况。也可以查看总的处理器负载情况。

mpstat 1

可以看到 %idle 处于闲置状态的 cpu 百分比,%user 用户态任务占用的 cpu 百分比,%sys系统态内核占用 cpu 百分比,%soft 软中断占用 cpu 百分比,%nice 调整任务优先级占用的 cpu 百分比等等。

iostat

主要用于监控io统计信息

iostat 1

如果我们有大量的 io 操作的话通过 iostat 监控 io 的写入和读取的数据量,同时也能看到在 io 负载特别大的情况下 cpu 的平均负载情况。

top

监控整个系统的整体性能情况top 命令是我们在日常情况下使用频率最高的,可以对当前系统环境了如指掌。处理器 load 率情况,memory 消耗情况,哪个 task 消耗 cpumemory最高。

top

top 命令功能非常丰富,可以分别根据 %MEM%CPU 排序。

load average 域表示 cpu load 率情况,后面三段分别表示最近1分钟、5分钟、15分钟的平均 load 率。这个值不能大于当前 cpu core 数,如果大于说明 cpu load 已经严重过高。就要去查看是不是线程数设置的过高,还要考虑这些任务是不是处理时间太长。设置的线程数与任务所处理的时长有直接关系。

Tasks 域表示任务数情况,total 总的任务数,running 运行中的任务数,sleeping 休眠中的任务数,stopped 暂停中的任务数,zombie 僵尸状态任务数。

Swap 域表示系统的交换区,压测的时候关注 used 是否会持续升高,如果持续升高说明物理内存已经用完开始进行内存页的交换。

free

查看当前系统的内存使用情况

free -m

total 总内存大小,used 已经分配的内存大小,free 当前可用的内存大小,shared 任务之间的共享内存大小,buffers 系统已经分配但是还未使用的,用来存放文件 matedata 元数据内存大小,cached 系统已经分配但是还未使用的,用来存放文件的内容数据的内存大小。

-/+buffer/cache

used 要减去 buffers/cached ,也就是说并没有用掉这么多内存,而是有一部分内存用在了 buffers/cached 里。

free 要加上 buffers/cached ,也就是说还有 buffers/cached 空余内存需要加上。

Swap 交换区统计,total 交换区总大小,used 已经使用的交换区大小,free 交换区可用大小。只需要关注 used 已经使用的交换区大小,如果这里有占用说明内存已经到瓶颈。

《深入理解LINUX内核》、《LINUX内核设计与实现》可以放在手边作为参考手册遇到问题翻翻。

性能排查两种方式(从上往下、从下往上)

当系统出现性能问题的时候可以从两个层面来排查问题,从上往下、从下网上,也可以综合运用这两种方法,压测的时候可以同时查看这两个纬度的信息。

一边打开 topfree 观察 cpumemory 的系统级别的消耗情况,同时一边在通过 jstackjstat 之类的工具查看应用程序运行时的内部状态来综合定位。

总结

本篇文章主要还是从抛砖引玉的角度出发,整理下我们在做一般性能压测的时候出现的常规问题及排查方法和处理流程,并没有多么高深的技术点。

性能问题一旦出现也不会是个简单的问题,都需要花费很多精力来排查问题,运用各种工具、命令来逐步排查,而这些工具和命令所输出的信息都是系统底层原理,需要逐一去理解和实验的,并没有一个银弹能解决所有问题。

 

Clean Code 阅读总结

1 开始

本文是在阅读 clean code 时的一些总结,原书是基于 Java 的,这里将其中的一些个人认为实用性较强且容易与日常业务开发结合的一些原则重新进行整理,并参考了 clean-code-javascript 一文给出了一些代码实例,希望本文能够给日常开发编码和重构作出一些参考。

2 有意义的命名

2.1 名副其实

变量取名要花心思想想,不要贪图方便,过于简略的名称,时间长了以后就难以读懂。

// bad
var d = 10;
var oVal = 20;
var nVal = 100;


// good
var days = 10;
var oldValue = 20;
var newValue = 100;

2.2 避免误导

命名不要让人对变量的信息 (类型,作用) 产生误解。

accounts 和 accountList,除非 accountList 真的是一个 List 类型,否则 accounts 会比 accountList 更好。因此像 List,Map 这样的后缀,不要随意使用。

// bad
var platformList = {
    web: {},
    wap: {},
    app: {},
};


// good
var platforms = {
    web: {},
    wap: {},
    app: {},
};

2.3 做有意义的区分

用明确的意义去表述变量直接的区别。

很多情况下,会有存在 product,productData,productInfo 之类的命名,Data 和 Info 很多情况下并没有明显的区别,不如直接就使用 product。

// bad
var goodsInfo = {
    skuDataList: [],
};

function getGoods(){};          // 获取商品列表
function getGoodsDetail(id){};  // 通过商品ID获取单个商品


// good
var goods = {
    skus: [],
};

function getGoodsList(){};      // 获取商品列表
function getGoodsById(id){};    // 通过商品ID获取单个商品

2.4 使用读得出来的名称

缩写要有个度,比如像 DAT 这样的写法,到底是 DATA 还是 DATE…

// bad
var yyyyMMddStr = eu.format(new Date(), 'yyyy-MM-dd');
var dat = null;
var dev = 'Android';


// good
var todaysDate = eu.format(new Date(), 'yyyy-MM-dd');
var data = null;
var device = 'Android';

2.5 使用可搜索的名称

可搜索的名称能够帮助快速定位代码,尤其对于一些数字状态码,不建议直接使用数值,而是使用枚举。

// bad
var param = {
    periodType: 0,
};


// good
const HOUR = 0, DAY = 1;
var param = {
    periodType: HOUR,
};

2.6 避免使用成员前缀

把类和函数做得足够小,消除对成员前缀的需要。因为长期以后,前缀在人们眼里会变得越来越不重要。

2.7 添加有意义的语境

对于某些名称,在不同语境下可能代表不同的含义,最好为它添加有意义的语境。

firstName,lastName,street,houseNumber,city,state,zipcode 一连串变量放在一起可以判断是一个地址,但是如果将这些变量单独拎出来,有些变量名意义就不明确了。这时可以添加语境明确其意义,如 addrFirstName,addrLastName,addrState。

当然也不要随意添加语境,这样只会让变量名变得冗长。

// bad
var firsName, lastName, city, zipcode, state;
var sku = {
    skuName: 'sku0',
    skuStorage: 'storage0',
    skuCost: '10',
};


// good
var addrFirsName, addrLastName, city, zipcode, addrState;
var sku = {
    name: 'sku0',
    storage: 'storage0',
    cost: '10',
};

2.8 变量名从一而终

变量名取名多花一点时间,如果这一对象会在多个函数,模块中使用,就应该使用一致的变量名,否则每次看到这个对象,都需要重新去理清变量名,造成阅读障碍。

// bad
function searchGoods(searchText) {
    getList({
        keyword: searchText,
    });
}
function getList(option) {

}

// good
function searchGoods(keyword) {
    getList({
        keyword: keyword,
    });
}

function getList(keyword) {}

3 函数

3.1 短小

短小是函数的第一规则,过长的函数不仅会造成阅读困难,在维护的时候难度也会增加。短小,要求每个函数做尽可能少的事情,同时减少代码的嵌套和缩进,要知道,代码的嵌套和缩减同样会带来阅读的困难。

// bad
function initPage(initParams) {
    var data = this.data;
    if ('dimension' in initParams) {
        data.dimension = initParams.dimension;
        data.tab.source.some(function(item, index){
            if (item.value === data.dimension) {
                data.tab.defaultIndex = index;
            }
        });
    }
    if ('standardMedium' in initParams) {
        data.hasStandardMedium = true;
        data.filterParams[data.dimension].standardMedium = initParams.standardMedium;
    }
    if ('plan' in initParams || 'name' in initParams) {
        data.filterParams[data.dimension].planQueryString = initParams.plan || initParams.name;
    } else if ('traceId' in initParams) {
        data.filterParams[data.dimension].planQueryString = 'id:' + initParams.traceId;
    }
}

// good
function initPage(initParams) {
    initDimension(initParams);
    initStandardMedium(initParams);
    initPlanQueryString(initParams);
}
function initDimension(initParams) {
    var data = this.data;
    if ('dimension' in initParams) {
        data.dimension = initParams.dimension;
        data.tab.source.some(function(item, index){
            if (item.value === data.dimension) {
                data.tab.defaultIndex = index;
            }
        });
    }
}
function initStandardMedium(initParams) {
    var data = this.data;
    if ('standardMedium' in initParams) {
        data.hasStandardMedium = true;
        data.filterParams[data.dimension].standardMedium = initParams.standardMedium;
    }
}
function initPlanQueryString() {
    var data = this.data;
    if ('plan' in initParams || 'name' in initParams) {
        data.filterParams[data.dimension].planQueryString = initParams.plan || initParams.name;
    } else if ('traceId' in initParams) {
        data.filterParams[data.dimension].planQueryString = 'id:' + initParams.traceId;
    }
}

3.2 只做一件事情

函数应该做一件事情,做好这件事,只做这一件事。

如果函数只是做了该函数名下同一个抽象层上的步骤,则函数还是只做了一件事。当函数中出现另一抽象层级所做的事情时,则可以将这部分拆成另一层级的函数,因此缩小函数。

当一个函数可以被划分成多个区段时(代码块)时,这就说明了这个函数做了太多事情。

// bad
function onTimepickerChange(type, e) {
    if(type === 'base') {
        // do base type logic...
    } else if (type === 'compare') {
        // do compare type logic...
    }
    // do other stuff...
}

// good
function onBaseTimepickerChange(e) {
    // do base type logic
    this.doOtherStuff();
}

function onCompareTimepickerChange(e) {
    // do compare type logic
    this.doOtherStuff();
}

function doOtherStuff(){}

3.3 每个函数一个抽象层级

一个函数中不应该混杂了多个抽象层级,即同一级别的步骤才放到一个函数中,因为通过这些步骤就能完整地完成一件事情。

回到之前提到变量命名的问题,一个变量或函数,其作用域余越广,就越需要一个有意义的名字来对其进行描述,提高可读性,减少在阅读代码时还需要去查询定义代码的频率,有些时候有意义的名字就可能需要更多的字符,但这是值得的。但对于小范围使用的变量和函数,可以适当缩短名称。因为过长的名称,某些时候反而会增加阅读的困难。

可以通过向下原则划分抽象层级

程序就像是一系列 TO 起头的段落,每一段都描述当前层级,并引用位于下一抽象层级的后续 TO 起头段落
- 如果要完成 A,需要完成 B,完成 C;
- 要完成 B,需要完成 D;
- 要完成 C,需要完成 E;

函数名明确了其作用,获取一个图表和列表,函数中各个模块的逻辑进行了划分,明确各个函数的分工, 拆分的函数名直接表明了每个步骤的作用, 不需要额外的注释和划分。在维护的时候, 可以快速的定位各个步骤, 而不需要在一个长篇幅的函数中需找对应的代码逻辑.

实际业务例子, 数据门户-流量看板-流量总览的一个获取趋势图和右边列表的例子。选择一个通过 tab 选择不同的指标,不同的指标影响的趋势图和右边列表的内容,两个模块的数据合并到一个请求中得到。流水账的写法可以将函数写成下面的样子,这种写法有几个明显的缺点:

  • 长。通常情况下趋势图配置可能就需要20多行,整个函数加起来,轻易就超过50行了;
  • 函数名不准确。函数名仅表明是获取一个图表的,但实际上还获取了右边列表数据并进行了配置;
  • 函数层级混乱,还可以进行更细的划分;

根据向下原则

// bad
getChart: function(){
    var data = this.data;
    var option = {
        url: '/chartUrl',
        param: {
            dimension: data.dimension,
            period: data.period,
            comparePeriod: data.comparePeriod,
            periodType: data.periodType,
        },
        fn: function(json){
            var data = this.data;
            // 设置图表
            data.chart = json.data.chart;
            data.chart.config = {
                //... 大量的图表配置,可能有20多行
            }
            // 设置右边列表
            data.sideList = json.data.list;
        }
    };
    // 获取请求参数
    this.fetchData(option);
},

// good
getChartAndSideList: function(){
    var option = {
        url: '/chartUrl',
        param: this.getChartAndSideListParam();
        fn: function(json){
            this.setChart(json);
            this.setSideList(json);
        }
    };
    this.fetchData(option);
},

3.4 switch语句

switch语句会让代码变得很长,因为switch语句天生就是要做多件事情,当状态不断增加的时候,switch语句也会不断增加。因此可能把取代switch语句,或者将其放在较低的层级.

放在底层的意思,可以理解为将其埋藏到抽象工厂地下,利用抽象工厂返回内涵不同的方法或对象来进行处理.

3.5 减少函数的参数

函数的参数越多,不仅注释写得长,使用的时候容易使得函数参数发生错位。当函数参数过多时,可以考虑以参数列表或者对象的形式传入.

数据门户里面的一个例子:

// bad
function getSum(a [, b, c, d, e ...]){}


// good
function getSum(arr){}
// bad
function exportExcel(url, param, onsuccess, onerror){}


// good
/**
 * @param option
 *    @property url
 *    @property param
 *    @property onsucces
 *    @property onerror
 */
function exportExcel(option){}

参数尽量少,最好不要超过 3 个

3.6 取个好名字

函数应该取个好一点的名字,适当使用动词和关键字可以提高函数的可读性。例如:

一个判断是否在某个区间范围的函数,取名为 within,从名称上可以容易判断出函数的作用,但是这仍然不是最好的,因为这个函数带有三个参数,无法一眼看出这个函数三个参数之间的关系,是 b <= a && a<= c,还是 a <= b && b <= c ?

或许可以通过更改参数名来表达三个参数的关系,这个必须看到函数的定义后才可能得知函数的用法.

如果再把名字改一下,从名字就可以容易得知三个参数依次的关系,当然这个名字可能会很长,但如果这个函数需要大范围地使用,较长的名字换来更好的可读性,这一代价是值得的.

// bad
function within(a, b, c){}

// good
function assertWithin(val, min, max){}

// good
function assertValWithinMinAndMax(val, min, max){}

3.7 无副作用

一个有副作用的函数,通常都是是非纯函数,这意味着函数做的事情其实不止一件,函数所产生的副作用被隐藏了,函数调用者无法直接通过函数名来明确函数所做的事请.

4 注释

4.1 好注释

法律信息,提供信息的注释,对意图的解释,阐释,警示,TODO,放大(放大某种看似不合理代码的重要性),公共 API 注释

尽量让函数,变量变得刻度,不要依赖注释来描述,对于复杂难懂的部分才适当用注释说明.

4.2 坏注释

喃喃自语,多余的注释(例如本来函数名就能够说明意图,还要加注释),误导性注释,循规式注释(为了规范去加注释,其实函数名和参数名已经可以明确信息了),日志式注释(记录无用修改日志的注释),废话注释

4.3 原则

  1. 能用函数或变量说明时,就别用注释,这就意味着要花点时间取个好名字
// bad
var d = 10;     // 天数

// good
var days = 10;
  1. 注释掉的代码不要留,重要的代码是不会被注释掉的

数据门户-实时概况里面的一段代码,/src/javascript/realTimeOverview/components/index.js

// bad
function dimensionChanged(dimension){
    var data = this.data.keyDealComposition;
    data.selectedDimension = dimension;
    // 2016.10.31 modify:产品改动,选择品牌分布的时候不显示二级类目
    // if (dimension.dimensionId == '6') {
    //     data.columns[0][0].name = dimension.dimensionName;
    //     data.columns[0].splice(1, 0, {name:'二级类目', value:'secCategoryName', noSort: true});
    // } else {
        this.handle('util.setTableHeader');
    // }
    this.handle('refreshComposition');
};

// good
function dimensionChanged(dimension){
    var data = this.data.keyDealComposition;
    data.selectedDimension = dimension;
    this.handle('util.setTableHeader');
    this.handle('refreshComposition');
};
  1. 不要在注释里面加入太多信息,没人会看
  2. 非公用函数,没有必要加过多的注释说明,冗余的注释会使代码变得不够紧凑,增加阅读障碍
// bad
/**
 * 设置表格表头
 */
function setTableHeader(){},

// good
function setTableHeader(){},
  1. 括号后的注释
// bad
function doSomthing(){
    while(!buffer.isEmpty()) {  // while 1
        // ...
        while(arr.length > 0) {  // while 2
            // ...
            if() {

            }
        } // while 2
    } // while 1
}
  1. 不需要日志式,归属式注释,相信版本控制系统
// bad
/**
 * 2016.12.03 bugfix, by xxxx
 * 2016.11.01 new feature, by xxxx
 * 2016.09.12 new feature, by xxxx
 * ...
 */


// bad
/**
 * created by xxxx
 * modified by xxxx
 */
function addSum() {}

/**
 * created by xxxx
 */
function getAverage() {
    // modified by xxx
}
  1. 尽量别用用位置标记
// bad

/*************** Filters ****************/

///////////// Initiation /////////////////

5 格式

5.1 垂直方向

  1. 相关代码紧凑显示,不同部分的用空格隔开
// bad
function init(){
    this.data.chartView = this.$refs.chartView;
    this.$parent.$on('inject', function () {
        this.dataConvert(this.data.source);
        this.draw();
    });
    this.$watch('source', function (newValue, oldValue) {
        if (newValue && newValue != this.data.initValue) {
            this.dataConvert(newValue);
            this.draw();
        } else if (!newValue) {
            if (self.data.chartView) {
                this.data.chartView.innerHTML = '';
            }
        }
    }, true);
}

// good
function init(){
    this.data.chartView = this.$refs.chartView;

    this.$parent.$on('inject', function () {
        this.dataConvert(this.data.source);
        this.draw();
    });

    this.$watch('source', function (newValue, oldValue) {
        if (newValue && newValue != this.data.initValue) {
            this.dataConvert(newValue);
            this.draw();
        } else if (!newValue) {
            if (this.data.chartView) {
                this.data.chartView.innerHTML = '';
            }
        }
    }, true);
}
  1. 不要在代码中加入太多过长的注释,阻碍代码阅读
// bad
BaseComponent.extend({
    checkAll: function(status){
        status = !!status;
        var data = this.data;
        this.checkAllList(status);
        this.checkSigList(status);
        data.checked.list = [];
        if(status){
            // 当全选的时候先清空列表, 然后在利用Array.push添加选中项
            // 如果在全选的时候不能直接checked.list = dataList
            // 因为这样的话后面对checked.list的操作就相当于对dataList直接进行操作
            // 利用push可以解决这一个问题
            data.sigList.forEach(function(item,i){
                data.checked.list.push(item.data.item);
            })
        }
        this.$emit('check', {
            sender: this,
            index: CHECK_ALL,
            checked: status,
        });
    },
});

// good
BaseComponent.extend({
    checkAll: function(status){
        status = !!status;
        this.checkAllList(status);
        this.checkSigList(status);
        this.clearCheckedList();
        if(status){
            this.updateCheckedList();
        }

        this.emitCheckEvent(CHECK_ALL, status);
    },
});
  1. 函数按照依赖顺序布局,被调用函数应该紧跟调用函数
// bad
function updateModule() {}
function updateFilter() {}
function reset() {}
function refresh() {
    updateFilter();
    updateModule();
}

// good
function refresh() {
    updateFilter();
    updateModule();
}
function updateFilter() {}
function updateModule() {}
function reset() {}
  1. 相关的,相似的函数放在一起
// bad
function onSubmit() {}
function refresh() {}
function onFilterChange() {}
function reset() {}

// good
function onSubmit() {}
function onFilterChange() {}

function refresh() {}
function reset() {}
  1. 变量声明靠近其使用位置
// bad
function (x){
    var a = 10, b = 100;
    var c, d;

    a = (a-b) * x;
    b = (a-b) / x;
    c = a + b;
    d = c - x;
}

// good
function (x){
    var a = 10, b = 100;

    a = (a-b) * x;
    b = (a-b) / x;

    var c = a + b;
    var d = c - x;
}

5.2 水平方向

  1. 运算符号之间空格,但是要注意运算优先级
// bad
var v = a + (b + c) / d + e * f;

// good
var v = a + (b+c)/d + e*f;
  1. 变量水平对齐意义不大,应该让其靠近
// bad
var a       = 1;
var sku     = goodsInfo.sku;
var goodsId = goodsInfo.goodsId;

// good
var a = 1;
var sku = goodsInfo.sku;
var goodsId = goodsInfo.goodsId;

5.4 对于短小的if,while语句,也要尽量保持缩进

突然间改变缩进的规律,很容易就会被阅读习惯欺骗

// bad
if(empty){return;}


// good
if(empty){
    return;
}

// bad
while(cli.readCommand() != -1);
app.run();


// good
while(cli.readCommand() != -1)
;

app.run();

6 实际业务代码中的应用

庞大的config函数

对于一些较为复杂的组件或页面组件,需要定义很多属性,同时又要对这部分属性进行初始化和监听,像下面这段代码。在好几个大型的页面里面都看到了类似的代码,config 方法少的有 100行,多的有 400行。

config 方法基本就是一个组件的入口,在进行维护的时候一般都会先读 config 方法,但是对于这么长的函数,很容易第一眼就懵了。

Component.extend({
    template: tpl,
    config: function(data){
        eu.extend(data, {
            tabChartTab: 0,
            periodType: 0,
            dimensionType: 1,
            dealConstituteCompare:false,
            dealConstituteSort: {
                dimensionValue: 'sales',
                sortType: 0,
            },
            dealConstituteDecorate: {
                noCompare:[],
                progress: ['salesPercent'],
                sort:[
                ]
            },
            defaultMetrics: [
            ],
            // ...下面还有几百行关于其他模块的属性, flow, hotSellRank等
        });

        this.$watch('periodType', function(){
            // ...
        });

        this.$watch('topCategoryId', function(){
            // ...
        });

        // 这里还有一部分异步请求代码...
        this.refresh();
    },
})

针对上述这段代码代码,明显的缺点是:

  • 太长
  • 变量命名有冗余信息,且搜索性差
  • 变量(属性)太多
  • 做的事情太多,初始化组件属性,添加监听方法,还有一些业务逻辑代码

这对这些可以作出一些改进:

  • 使用枚举代替数值
  • config内只保留一切作为范围加大属性的直接初始化代码,其余针对于模块的属性将通过调用 initData 方法来初始化
  • initData 进一步根据模块划分初始化方法
  • 对于属于摸个模块的属性,则将其划分到同一个对象上,减少组件上挂载的属性数量,同时也简化了属性的命名
  • 监听方法同样是通过 addWatchers 初始化
  • 初始化过程中需要执行的部分逻辑,尽可能放在 init 等组件实例化后执行
const TAB_A = 0, TAB_B = 1;
const HOUR = 0, DAY = 1;
const DIMENSION_A = 0, DIMENSION_B = 1;
const DISABLE = false, ENABLE = true;

Component.extend({
    template: tpl,
    config: function(data){
        eu.extend(data, {
            tabChartTab: TAB_A,
            periodType: HOUR,
            dimensionType: DIMENSION_B,
        });

        this.initData();
        this.addWatchers();
    },

    initData: function(){
        this.initDealConsitiuteData();
        this.initFlowData();
        this.initHotSellRank();
    },

    initDealConsitiuteData: function(){
        this.data.dealConstitute = {
            compare: DISABLE,
            sort: {
                dimensionValue: 'sales',
                sortType: 0,
            },
            decorate: {
                noCompare:[],
                progress: ['salesPercent'],
                sort:[
                ]
            },
            defaultMetrics: [
            ],
        }
    },

    addWatchers: function(){
        this.$watch('periodType', function(){
            // ...
        });

        this.$watch('topCategoryId', function(){
            // ...
        });
    },

    init: function(){
        // 部分初始化要执行的逻辑
        this.refresh();
    },

})

其实按照上面进行优化以后,代码的可读性是有所提高,但由于这是一个页面组件,代码行数极多,修改后方法变得更多了,仍然不便于阅读。所以,针对于这种大型的页面,更适当的做法是,将页面拆分为几个模块,将业务逻辑拆分,减少每个模块的代码量,提高可读性。而对于不可再拆分的组件或模块,如果仍然包含大量需要初始化的属性,上述例子就可以作为参考了。

7 总结

本文整理的几个要点:

  • 写代码就像写故事,里面各个角色 (变量,函数) 的名字要取得好,才读得流畅;
  • 函数要短小,不要混杂太多不相关,不同层级的逻辑;
  • 注释要精简准确,能不写就不要写;
  • 代码布局要向报纸学习,排版注意垂直与水平方向的间隔,联系紧密的布局要紧凑;

就算是经验老道的大神,也很难一遍就能写出简洁的代码,所以要勤于对代码进行重构,边写代码边修改。代码只有在经过一遍一遍修改和锤炼以后,才会逐渐地变得简洁和精致。

8 参考

  1. Clean Code
  2. clean-code-javascript

作者:网易考拉海购
链接:https://juejin.im/post/59c85c936fb9a00a4746de94
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

记一次JavaWeb网站技术架构总结

题记

工作也有几多年了,无论是身边遇到的还是耳间闻到的,多多少少也积攒了自己的一些经验和思考,当然,博主并没有太多接触高大上的分布式架构实践,相对比较零碎,随时补充(附带架构装逼词汇)。

俗话说的好,冰冻三尺非一日之寒,滴水穿石非一日之功,罗马也不是一天就建成的,当然对于我们开发人员来说,一个好的架构也不是一蹴而就的。

初始搭建

开始的开始,就是各种框架一搭,然后扔到Tomcat容器中跑就是了,这时候我们的文件,数据库,应用都在一个服务器上。

服务分离

随着系统的的上线,用户量也会逐步上升,很明显一台服务器已经满足不了系统的负载,这时候,我们就要在服务器还没有超载的时候,提前做好准备。

由于我们是单体架构,优化架构在短时间内是不现实的,增加机器是一个不错的选择。这时候,我们可能要把应用和数据库服务单独部署,如果有条件也可以把文件服务器单独部署。

反向代理

为了提升服务处理能力,我们在Tomcat容器前加一个代理服务器,我一般使用Nginx,当然你如果更熟悉apache也未尝不可。

用户的请求发送给反向代理,然后反向代理把请求转发到后端的服务器。

严格意义上来说,Nginx是属于web服务器,一般处理静态html、css、js请求,而Tomcat属于web容器,专门处理JSP请求,当然Tomcat也是支持html的,只是效果没Nginx好而已。

反向代理的优势,如下:

  • 隐藏真实后端服务
  • 负载均衡集群
  • 高可用集群
  • 缓存静态内容实现动静分离
  • 安全限流
  • 静态文件压缩
  • 解决多个服务跨域问题
  • 合并静态请求(HTTP/2.0后已经被弱化)
  • 防火墙
  • SSL以及http2

动静分离

基于以上Nginx反向代理,我们还可以实现动静分离,静态请求如html、css、js等请求交给Nginx处理,动态请求分发给后端Tomcat处理。

Nginx 升级到1.9.5+可以开启HTTP/2.0时代,加速网站访问。

当然,如果公司不差钱,CDN也是一个不错的选择。

服务拆分

在这分布式微服务已经普遍流行的年代,其实我们没必要踩过多的坑,就很容易进行拆分。市面上已经有相对比较成熟的技术,比如阿里开源的Dubbo(官方明确表示已经开始维护了),spring家族的spring cloud,当然具体如何去实施,无论是技术还是业务方面都要有很好的把控。

Dubbo

SpringCloud
  • 服务发现——Netflix Eureka
  • 客服端负载均衡——Netflix Ribbon
  • 断路器——Netflix Hystrix
  • 服务网关——Netflix Zuul
  • 分布式配置——Spring Cloud Config
微服务与轻量级通信
  • 同步通信和异步通信
  • 远程调用RPC
  • REST
  • 消息队列

持续集成部署

服务拆分以后,随着而来的就是持续集成部署,你可能会用到以下工具。

Docker、Jenkins、Git、Maven

图片源于网络,基本拓扑结构如下所示:

整个持续集成平台架构演进到如下图所示:

服务集群

Linux集群主要分成三大类( 高可用集群, 负载均衡集群,科学计算集群)。其实,我们最常见的也是生产中最常接触到的就是负载均衡集群。

负载均衡实现

  • DNS负载均衡,一般域名注册商的dns服务器不支持,但博主用的阿里云解析已经支持
  • 四层负载均衡(F5、LVS),工作在TCP协议下
  • 七层负载均衡(Nginx、haproxy),工作在Http协议下

分布式session

大家都知道,服务一般分为有状态和无状态,而分布式sessoion就是针对有状态的服务。

分布式Session的几种实现方式
  • 基于数据库的Session共享
  • 基于resin/tomcat web容器本身的session复制机制
  • 基于oscache/Redis/memcached 进行 session 共享。
  • 基于cookie 进行session共享
分布式Session的几种管理方式
  • Session Replication 方式管理 (即session复制)
    简介:将一台机器上的Session数据广播复制到集群中其余机器上
    使用场景:机器较少,网络流量较小
    优点:实现简单、配置较少、当网络中有机器Down掉时不影响用户访问
    缺点:广播式复制到其余机器有一定廷时,带来一定网络开销
  • Session Sticky 方式管理
    简介:即粘性Session、当用户访问集群中某台机器后,强制指定后续所有请求均落到此机器上
    使用场景:机器数适中、对稳定性要求不是非常苛刻
    优点:实现简单、配置方便、没有额外网络开销
    缺点:网络中有机器Down掉时、用户Session会丢失、容易造成单点故障
  • 缓存集中式管理
    简介:将Session存入分布式缓存集群中的某台机器上,当用户访问不同节点时先从缓存中拿Session信息
    使用场景:集群中机器数多、网络环境复杂
    优点:可靠性好
    缺点:实现复杂、稳定性依赖于缓存的稳定性、Session信息放入缓存时要有合理的策略写入
目前生产中使用到的
  • 基于tomcat配置实现的MemCache缓存管理session实现(麻烦)
  • 基于OsCache和shiro组播的方式实现(网络影响)
  • 基于spring-session+redis实现的(最适合)

负载均衡策略

负载均衡策略的优劣及其实现的难易程度有两个关键因素:一、负载均衡算法,二、对网络系统状况的检测方式和能力。

1、rr 轮询调度算法。顾名思义,轮询分发请求。

优点:实现简单

缺点:不考虑每台服务器的处理能力

2、wrr 加权调度算法。我们给每个服务器设置权值weight,负载均衡调度器根据权值调度服务器,服务器被调用的次数跟权值成正比。

优点:考虑了服务器处理能力的不同

3、sh 原地址散列:提取用户IP,根据散列函数得出一个key,再根据静态映射表,查处对应的value,即目标服务器IP。过目标机器超负荷,则返回空。

4、dh 目标地址散列:同上,只是现在提取的是目标地址的IP来做哈希。

优点:以上两种算法的都能实现同一个用户访问同一个服务器。

5、lc 最少连接。优先把请求转发给连接数少的服务器。

优点:使得集群中各个服务器的负载更加均匀。

6、wlc 加权最少连接。在lc的基础上,为每台服务器加上权值。算法为:(活动连接数*256+非活动连接数)÷权重 ,计算出来的值小的服务器优先被选择。

优点:可以根据服务器的能力分配请求。

7、sed 最短期望延迟。其实sed跟wlc类似,区别是不考虑非活动连接数。算法为:(活动连接数+1)*256÷权重,同样计算出来的值小的服务器优先被选择。

8、nq 永不排队。改进的sed算法。我们想一下什么情况下才能“永不排队”,那就是服务器的连接数为0的时候,那么假如有服务器连接数为0,均衡器直接把请求转发给它,无需经过sed的计算。

9、LBLC 基于局部性的最少连接。均衡器根据请求的目的IP地址,找出该IP地址最近被使用的服务器,把请求转发之,若该服务器超载,最采用最少连接数算法。

10、LBLCR 带复制的基于局部性的最少连接。均衡器根据请求的目的IP地址,找出该IP地址最近使用的“服务器组”,注意,并不是具体某个服务器,然后采用最少连接数从该组中挑出具体的某台服务器出来,把请求转发之。若该服务器超载,那么根据最少连接数算法,在集群的非本服务器组的服务器中,找出一台服务器出来,加入本服务器组,然后把请求转发之。

读写分离

MySql主从配置,读写分离并引入中间件,开源的MyCat,阿里的DRDS都是不错的选择。

如果是对高可用要求比较高,但是又没有相应的技术保障,建议使用阿里云的RDS或者Redis相关数据库,省事省力又省钱。

全文检索

如果有搜索业务需求,引入solr或者elasticsearch也是一个不错的选择,不要什么都塞进关系型数据库。

缓存优化

引入缓存无非是为了减轻后端数据库服务的压力,防止其”罢工”。

常见的缓存服务有,Ehcache、OsCache、MemCache、Redis,当然这些都是主流经得起考验的缓存技术实现,特别是Redis已大规模运用于分布式集群服务中,并证明了自己优越的性能。

消息队列

异步通知:比如短信验证,邮件验证这些非实时反馈性的逻辑操作。

流量削锋:应该是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。

日志处理:系统中日志是必不可少的,但是如何去处理高并发下的日志确是一个技术活,一不小心可能会压垮整个服务。工作中我们常用到的开源日志ELK,为嘛中间会加一个Kafka或者redis就是这么一个道理(一群人涌入和排队进的区别)。

消息通讯:点对点通信(个人对个人)或发布订阅模式(聊天室)。

日志服务

消息队列中提到的ELK开源日志组间对于中小型创业供公司是一个不错的选择。

安全优化

以上种种,没有安全做保证可能都会归于零。

  • 阿里云的VPN虚拟专有网络以及安全组配置
  • 自建机房的话,要自行配置防火墙安全策略
  • 相关服务访问,比如Mysql、Redis、Solr等如果没有特殊需求尽量使用内网访问并设置鉴权
  • 尽量使用代理服务器,不要对外开放过多的端口
  • https配合HTTP/2.0也是个不错的选择

架构装逼必备词汇

高可用

  • 负载均衡(负载均衡算法)
  • 反向代理
  • 服务隔离
  • 服务限流
  • 服务降级(自动优雅降级)
  • 失效转移
  • 超时重试
  • 回滚机制

高并发

  • 应用缓存
  • HTTP缓存
  • 多级缓存
  • 分布式缓存
  • 连接池
  • 异步并发

分布式事务

  • 二阶段提交(强一致)
  • 三阶段提交(强一致)
  • 消息中间件(最终一致性),推荐阿里的RocketMQ

队列

  • 任务队列
  • 消息队列
  • 请求队列

扩容

  • 单体垂直扩容
  • 单体水平扩容
  • 应用拆分
  • 数据库拆分
  • 数据库分库分表
  • 数据异构
  • 分布式任务

网络安全

  • SQL注入
  • XSS攻击
  • CSRF攻击
  • 拒绝服务(DoS,Denial of Service)攻击

架构装逼必备工具

操作系统

Linux(必备)、某软的

负载均衡

DNS、F5、LVS、Nginx、HAproxy、负载均衡SLB(阿里云)

分布式框架

Dubbo、Motan、Spring-Could

数据库中间件

DRDS (阿里云)、Mycat、360 Atlas、Cobar (不维护了)

消息队列

RabbitMQ、ZeroMQ、Redis、ActiveMQ、Kafka

注册中心

Zookeeper、Redis

缓存

Redis、Oscache、Memcache、Ehcache

集成部署

Docker、Jenkins、Git、Maven

存储

OSS、NFS、FastDFS、MogileFS

数据库

MySql、Redis、MongoDB、PostgreSQL、Memcache、HBase

网络

专用网络VPC、弹性公网IP、CDN