递归 (JavaScript && PHP)

JavaScript

// $PATH utils/recursive.js
const recursive = (array, parent = 0) => {
  const newArray = []
  array.forEach(ele => {
    if (ele.parent === parent) {
      const item = {
        id: ele.id,
        label: ele.name,
        value: ele.id,
        parent: ele.parent
      }
      const children = recursive(array, ele.id)
      if (children.length > 0) {
        item.children = children
      }
      newArray.push(item)
    }
  })
  return newArray
}

export default recursive

测试一下

import recursive from '@/utils/recursive' // 放在utils目录里
const cats = [
  {
    id: 1,
    name: 'name1',
    parent: 0
  },
  {
    id: 11,
    name: 'name11',
    parent: 1
  },
  {
    id: 12,
    name: 'name12',
    parent: 1
  },
  {
    id: 121,
    name: 'name121',
    parent: 12
  },
  {
    id: 122,
    name: 'name122',
    parent: 12
  },
  {
    id: 2,
    name: 'name2',
    parent: 0
  }
]

const recursiveCats = [
  {
    'children': [
      {
        'id': 11,
        'label': 'name11',
        'parent': 1,
        'value': 11
      },
      {
        'children': [
          {
            'id': 121,
            'label': 'name121',
            'parent': 12,
            'value': 121
          },
          {
            'id': 122,
            'label': 'name122',
            'parent': 12,
            'value': 122
          }
        ],
        'id': 12,
        'label': 'name12',
        'parent': 1,
        'value': 12
      }
    ],
    'id': 1,
    'label': 'name1',
    'parent': 0,
    'value': 1
  },
  {
    'id': 2,
    'label': 'name2',
    'parent': 0,
    'value': 2
  }
]

describe('Utils:recursive', () => {
  it('recursive cats', () => {
    expect(recursive(cats)).toEqual(recursiveCats)
  })
})

PHP

function myRecursive(array $array = [], int $parent = 0)
{
    $newArray = [];
    foreach ($array as $item) {
        if ($item['parent'] === $parent) {
            $newItem = [
                'id'        => $item['id'],
                'label'     => $item['name'],
                'value'     => $item['id'],
                'parent'    => $item['parent']
            ];
            $children = myRecursive($array, $item['id']);
            if (count($children)) {
                $newItem['children'] = $children;
            }
            $newArray[] = $newItem;
        }
    }
    return $newArray;
}

/**
* 通过引用
*/
function myReferenceRecursive($items = [])
{
    $tree = [];
    foreach ($items as $key => $item) {
        if (isset($items[$item['parent']])) {
            $items[$item['parent']]['children'][] = &$items[$key];
        } else {
            $tree[] = &$items[$key];
        }
    }
    return $tree;
}

// 测试 test
$cats = '[
    {
        "id": 1,
        "name": "name1",
        "parent": 0
    },
    {
        "id": 11,
        "name": "name11",
        "parent": 1
    },
    {
        "id": 12,
        "name": "name12",
        "parent": 1
    },
    {
        "id": 121,
        "name": "name121",
        "parent": 12
    },
    {
        "id": 122,
        "name": "name122",
        "parent": 12
    },
    {
        "id": 2,
        "name": "name2",
        "parent": 0
    }
]';

$cats = json_decode($cats, true);
print_r(myRecursive($cats));

Nginx fastcgi_cache And proxy_cache

FastCGI Cache Example

FastCGI Cache VS WP Super Cache

FastCGI cache is faster than WP Super Cache because the latter uses .htaccess and PHP itself to route the visitor to the cache (files). So before the visitors get to a hit on a cached page, WP Super Cache has to perform some logic both in the form of .htaccess (rewrites) and PHP itself (all WP plugins must use PHP). Whereas FastCGI uses compiled bindings that are fast and routes traffic directly to the cache.

Now, an HTTP cache like Varnish will always be faster than the two. That’s because you’re traveling one fewer hop down the stack to get to the data, which in the case of Varnish sits at the HTTP level as an HTML object and can be served hot.

So to illustrate:

So to get to the:

FastCGI cache: Nginx -> FastCGI -> File

WP Super Cache cache: Nginx -> FastCGI -> PHP -> File

通常你需要做的修改在这个文件 /etc/nginx/sites-available/default

#move next 4 lines to /etc/nginx/nginx.conf if you want to use fastcgi_cache across many sites 
fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header http_500;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
server {
	server_name example.com www.example.com;

	access_log   /var/log/nginx/example.com.access.log;
	error_log    /var/log/nginx/example.com.error.log;

	root /var/www/example.com/htdocs;
	index index.php;



	set $skip_cache 0;

	# POST requests and urls with a query string should always go to PHP
	if ($request_method = POST) {
		set $skip_cache 1;
	}   
	if ($query_string != "") {
		set $skip_cache 1;
	}   

	# Don't cache uris containing the following segments
	if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
		set $skip_cache 1;
	}   

	# Don't use the cache for logged in users or recent commenters
	if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
		set $skip_cache 1;
	}

	location / {
		try_files $uri $uri/ /index.php?$args;
	}    



	location ~ \.php$ {
		try_files $uri =404; 
		include fastcgi_params;
		fastcgi_pass 127.0.0.1:9000;

		fastcgi_cache_bypass $skip_cache;
	        fastcgi_no_cache $skip_cache;

		fastcgi_cache WORDPRESS;
		fastcgi_cache_valid  60m;
	}

	location ~ /purge(/.*) {
	    fastcgi_cache_purge WORDPRESS "$scheme$request_method$host$1";
	}	

	location ~* ^.+\.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
		access_log off;	log_not_found off; expires max;
	}

	location = /robots.txt { access_log off; log_not_found off; }
	location ~ /\. { deny  all; access_log off; log_not_found off; }
}

Proxy Cache Example

# 申明缓存地址,名字和相关变量
proxy_cache_path /var/cache/nginx/microcache levels=1:2 keys_zone=microcache:10M max_size=10g inactive=2h use_temp_path=off;

server {
    listen       80;
    server_name  localhost;

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    location / {
        proxy_cache_valid 200       1d;
        proxy_cache microcache;
        proxy_cache_lock on;
        proxy_cache_lock_timeout    10s;
        proxy_cache_valid 301 302   10m;
        proxy_cache_valid 404       10s;
        proxy_cache_min_uses 1;
        proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
        proxy_ignore_headers Cache-Control Expires Set-Cookie;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X_FORWARDED_PROTO https;
        proxy_ssl_verify off;
        proxy_connect_timeout 200;
        proxy_send_timeout 300;
        proxy_read_timeout 300;

        add_header X-Cache-Status $upstream_cache_status;
        proxy_set_header Host www.yaoin.net;
        proxy_pass   https://47.98.200.34/;
        send_timeout 300;
    }
}

PHP 质因数分解

  1. 找出比 $number 小的质素
  2. 再分别除以比 $number 小的质素找出质因子
<?php
$stime = microtime(true); #获取程序开始执行的时间

class PrimeNumber
{

    /**
     * 判断 $number 是否为质素
     *
     * @param [type] $number
     * @return boolean
     */
    static function isPrime($number)
    {
        if ($number == 1) return false;

        if ($number == 2 || $number == 3) return true;

        for ($i = 2; $i <= sqrt($number); $i++) {
            if ($number % $i == 0)
                return false;
        }
        return true;
    }

    /**
     * 找出比 $number 小的质素
     *
     * @param [type] $number
     * @return void
     */
    function getPrimes($number)
    {
        $result = [];
        for ($i = 2; $i <= $number; $i++) {
            if (self::isPrime($i)) {
                $result[] = $i;
            }
        }

        return $result;
    }

    /**
     * 分别除以比 $number 小的质素
     *
     * @param integer $number
     * @return void
     */
    function Factorization(int $number)
    {
        if (self::isPrime($number)) {
            return $number . " is Prime Number";
        }
        $result = [];
        if ($number % 2 == 0) $result[] = 2;

        $primes = $this->getPrimes(ceil($number / 2));

        for ($i = 0; $i < count($primes); $i++) {
            if ($number % $primes[$i] == 0) {
                $result[] = $primes[$i];
            }
        }

        return [
            'number' => $number,
            'factors' => $result,
            'primes' => $primes
        ];
    }
}

$factors = (new PrimeNumber())->Factorization(65535);
print_r($factors);

$etime = microtime(true); #获取程序执行结束的时间
$total = $etime - $stime;   #计算差值

echo "\n{$total} times \n";

测试一下

PHP 模拟 RSA 算法

  1. 从 1-10 中间任意取两个 质素 p 和 q
  2. 让 p*q = n
  3. funN = (p-1) * (q-1)
  4. 获取 公钥 e:
    – 1 < e < funN
    – e 和 funN 互质
  5. 获取 私钥 d:使得 (e * d) % funN = 1
  6. 任意拿一个数字 m 用公钥匙 e 对其加密:pow(m, e) % n = c,c 即加密后的信息
  7. 用私钥 d 对c 解密:pow(c, d) % n = m, m 得到还原

这只是个模式,实际应用请参考:http://phpseclib.sourceforge.net/rsa/2.0/examples.html

<?php

class RSA
{
    protected $primes = [];
    protected $min;
    protected $max;
    protected $p;
    protected $q;
    protected $n;
    protected $funN;
    public $e; // public key
    public $d; // private key

    public function __construct($min, $max)
    {
        $this->min = $min;
        $this->max = $max;
    }

    public function isPrime($number)
    {
        if ($number < 2) return false;

        if ($number == 2 || $number == 3) return true;

        for ($i = 2; $i <= sqrt($number); $i++) {
            if ($number % $i == 0) return false;
        }

        return true;
    }

    public function getPrimes()
    {
        for ($i = $this->min; $i <= $this->max; $i++) {

            if ($this->isPrime($i)) {
                $this->primes[] = $i;
            }
        }

        return $this->primes;
    }

    public function setData()
    {

        if (count($this->primes) < 1) {
            $this->getPrimes();
        }

        $randomKeys = array_rand($this->primes, 2);
        $this->p = $this->primes[$randomKeys[0]];
        $this->q = $this->primes[$randomKeys[1]];
        $this->n = $this->p * $this->q;
        $this->funN = ($this->p - 1) * ($this->q - 1);
        $this->setPublicKey();
        if (!$this->e) {
            return (new self($this->min, $this->max))->setData();
        }
        $this->setPrivateKey();

        return $this;
    }

    public function getData()
    {
        return $this;
    }

    /**
     * get public key: 1 < $publicKey < $this->funN
     */
    public function setPublicKey()
    {
        for ($i = 1; $i < $this->funN; $i++) {
            if ($this->isPrime($i) && ($this->funN % $i  !== 0)) {
                $this->e = $i;
            }
        }
    }

    /**
     * ($publicKey * $privateKey) % $this->funN = 1
     * 
     * $publicKey * $privateKey > $this->funN
     * 
     * $privateKey > $this->funN / $publicKey
     */
    public function setPrivateKey()
    {
        $start = floor($this->funN / $this->e);
        $privateKey = 0;
        while ($privateKey == 0) {
            if (($this->e * $start) % $this->funN === 1) {
                $privateKey = $start;
                $this->d = $privateKey;
            }
            $start++;
        }
    }

    public function encrypt($number)
    {
        # code...
        return pow($number, $this->e) % $this->n;
    }

    public function decrypt($number)
    {
        return pow($number, $this->d) % $this->n;
    }
}


$rsa = (new RSA(1, 10))->setData();
print_r($rsa->getData());

$original = 3;
echo "original: " . $original . "\n";
$encrypt = $rsa->encrypt($original);
echo "encrypt message:" . $encrypt . "\n";

$decrypt = $rsa->decrypt($encrypt);
echo "decrypt message:" . $decrypt . "\n";

测试一下

使用 Vue 和 Vue-router 创建一个多布局(layout)系统

假设您创建了一个博客。您希望主页没有侧栏,而所有其他页面都有侧栏。

使用 vue-cli 3 创建一个项目:

vue create blog

创建 layouts 文件夹这 src。

创建有边栏的 Default 模版。

创建没有边栏的 NOSidebar 模版:

然后在 router.js 里创建页面:

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

import Home from './pages/Home'
import About from './pages/About'

const routes = [
    {
        path: '/',
        name: 'home',
        meta: {
            layout: 'no-sidebar'
        },
        component: Home
    },
    {
        path: '/about',
        name: 'about',
        meta: {
            layout: 'default'
        },
        component: About
    },
]

const router = new VueRouter({
    mode: 'history',
    routes
})

export default router

我为Home & About页面添加了一个meta键 在Vue-router中。

现在我们在 main.js 导入 layout

https://itnext.io/anyway-heres-how-to-create-a-multiple-layout-system-with-vue-and-vue-router-b379baa91a05

JWT authentication for Lumen

参考:https://medium.com/tech-tajawal/jwt-authentication-for-lumen-5-6-2376fd38d454

1.Add JWT_SECRET=xxxx to yours.env file

APP_KEY=1111 
JWT_SECRET=111

2. Create a migration file for the users table:

php artisan make:migration create_users_table

3. Modify the migration file created inside the database/migrations directory

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

4. create the seeder to populate the database with some users. Modify database/seeds/UsersTableSeeder.php to look like:

    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('users')->insertOrIgnore(
            [
                [
                    'name' => 'Admin',
                    'email' => 'administrator@app.com',
                    'password' => Hash::make('password'),
                ],
                [
                    'name' => 'Editor',
                    'email' => 'editor@app.com',
                    'password' => Hash::make('password'),
                ],
                [
                    'name' => 'User',
                    'email' => 'user@app.com',
                    'password' => Hash::make('password'),
                ]
            ]
        );
    }

5. Now create the configured database in MySQL and run the following commands inside your terminal to create the users table and add some dummy data respectively:

php artisan migrate
php artisan db:seed

Now your database looks something like this:

6. Lumen does not have facades and eloquent enabled by default, let’s enable them first by opening the bootstrap/app.php file and uncomment the following lines:

$app->withFacades();
$app->withEloquent();

7. Now let’s create the endpoint to generate JWT token. There are tons of libraries out there that will help you with it we will use the one called firebase/php-jwt. Open up your terminal and run the following command to pull it in using composer:

composer require firebase/php-jwt

8. Now let’s add the endpoint POST /auth/v1/login that will accept the credentials and return a token for us. Let’s register the route first by adding the following route inside routes/web.php file:

$router->group(['prefix' => 'auth/v1'], function () use ($router) {
    $router->post('login', 'AuthController@login');
});

8. Now we need the controller AuthController with method authenticate. Inside app/Http/Controllers folder create a new AuthController.php file and put following content inside it:

<?php

namespace App\Http\Controllers;

use Validator;
use App\User;
use Firebase\JWT\JWT;
use Illuminate\Http\Request;
use Firebase\JWT\ExpiredException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;

class AuthController extends Controller
{
    private $request;
    /**
     * Create a new AuthController instance.
     *
     * @return void
     */
    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    /**
     * Get a JWT via given credentials.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(User $user)
    {
        $this->validate($this->request, [
            'email'     => 'required|email',
            'password'  => 'required'
        ]);
        // Find the user by email
        $user = User::where('email', $this->request->input('email'))->first();
        if (!$user) {
            // You wil probably have some sort of helpers or whatever
            // to make sure that you have the same response format for
            // differents kind of responses. But let's return the
            // below respose for now.
            return response()->json([
                'error' => 'Email does not exist.'
            ], 400);
        }
        // Verify the password and generate the token
        if (Hash::check($this->request->input('password'), $user->password)) {
            return response()->json([
                'token' => $this->jwt($user),
                'token_type' => 'bearer',
                'data' => $user
            ], 200);
        }
        // Bad Request response
        return response()->json([
            'error' => 'Email or password is wrong.'
        ], 400);
    }

    /**
     * Get the authenticated User.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        $user = Auth::user();
        return response()->json($user);
    }

    /**
     * Create a new token.
     *
     * @param  \App\User   $user
     * @return string
     */
    protected function jwt(User $user)
    {
        $payload = [
            'iss' => "lumen-jwt", // Issuer of the token
            'sub' => $user->id, // Subject of the token
            'iat' => time(), // Time when JWT was issued.
            'exp' => time() + 60 * 60 // Expiration time
        ];

        // As you can see we are passing `JWT_SECRET` as the second parameter that will
        // be used to decode the token in the future.
        return JWT::encode($payload, env('JWT_SECRET'));
    }
}

10. Let’s open up your terminal and then run the application by running the following command:

php -S localhost:8000 -t public

11. Now to test our application i am using Postman. Inside postman create a new request to http://localhost:8000/auth/v1/login. When you hist this route by clicking the send button you will get the the token in response body.

12. Authentication

Uncomment lines in bootstrap/app.php

$app->routeMiddleware([
    'auth' => App\Http\Middleware\Authenticate::class,
]);
......
$app->register(App\Providers\AuthServiceProvider::class);

13. Now let’s protect some of our routes. Open the routes file i.e. routes/web.php and put the following routes inside it:

$router->group(['middleware' => 'auth'], function () use ($router) {
    $router->group(['prefix' => 'auth/v1'], function () use ($router) {
        $router->post('me', 'AuthController@me');
    });
});

14. After updating the routes file test if our request succeeds by hitting http://localhost:8000/auth/v1/me route. This request will fail because this is a protected route and require us to provide a token.

15. let’s edit App\HTTP\Middleware\Authenticate.php

<?php

namespace App\Http\Middleware;

use App\User;
use Closure;
use Exception;
use Firebase\JWT\JWT;
use Firebase\JWT\ExpiredException;
use Illuminate\Contracts\Auth\Factory as Auth;

class Authenticate
{
    /**
     * The authentication guard factory instance.
     *
     * @var \Illuminate\Contracts\Auth\Factory
     */
    protected $auth;

    /**
     * Create a new middleware instance.
     *
     * @param  \Illuminate\Contracts\Auth\Factory  $auth
     * @return void
     */
    public function __construct(Auth $auth)
    {
        $this->auth = $auth;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
        // check dose token provided
        $token = $request->header('authorization');
        if (is_null($token)) {
            // Unauthorized response if token not there
            return response([
                'code' => 'no_token',
                'message' => 'Token not provided.',
                'data' => [
                    'status' => 401
                ]
            ], 401);
        }
        // decode and check Token
        $token = str_replace('Bearer ', '', $token);
        try {
            $token = str_replace('Bearer ', '', $token);
            $credentials = JWT::decode($token, env('JWT_SECRET'), ['HS256']);

            $user = User::find($credentials->sub);
            if (!$user) {
                return response([
                    'code' => 'user_not_found',
                    'message' => 'User not found',
                    'data' => [
                        'status' => 400,
                    ]
                ], 400);
            }
            // Now let's put the user in the request class so that you can grab it from there
            $request->auth = $user;
        } catch (ExpiredException $e) {
            return response([
                'code' => 'token_expired',
                'message' => 'Provided token is expired.',
                'data' => [
                    'status' => 400,
                ]
            ], 400);
        } catch (Exception $e) {
            return response([
                'code' => 'decode_token_failed',
                'message' => 'An error while decoding token.',
                'data' => [
                    'status' => 400,
                ]
            ], 400);
        }

        return $next($request);
    }
}

在之前的 Middleware 里,我们一句吧 token 里的 ID 获取到的 User 对象传递到 $request 里,现在到APP\Providers\AuthServiceProvider.php ,修改 boot() 为:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Boot the authentication services for the application.
     *
     * @return void
     */
    public function boot()
    {
        $this->app['auth']->viaRequest('api', function ($request) {
            // recive current user object from middleware
            if ($request->auth) {
                return $request->auth;
            }
        });
    }
}

这样就可以通过 Auth::user() 获取当前用户信息。

16. make a request /auth/v2/me again:

使用 Intervention Image (PHP Package)修改图片大小

Intervention Image 是一个PHP图像处理和操作库,提供了一种更简单、更富表现力的方法来创建、编辑和组合图像。该包包括 ServiceProviders  和 Facades,便于Laravel集成。

这儿只是描述使用这个包来做一些最常用的图片截取调整大小的操作。

安装

composer require intervention/image

例子

// 引用自动加载类
require 'vendor/autoload.php';

// 引入 Intervention Image Manager 类
use Intervention\Image\ImageManager;

// create an image manager instance with favored driver
$manager = new ImageManager(array('driver' => 'imagick'));

// 实例化一个原始图片对象 to finally create image instances
$image = $manager->make('media/foo.jpg');

// 调整大小resize image instance
$image->resize(320, 240);

// 加水印 insert a watermark
$image->insert('public/watermark.png');

// 保存处理后的图片的本地磁盘 save image in desired format
$image->save('public/cache/foo.jpg');

// 返回缓存图片地址,用来显示

关于 调整大小

fit – 裁剪和调整大小相结合

// 打开图片资源 open file a image resource
$img = Image::make('public/foo.jpg');

// 裁剪最合适的5:3 (600x360)的比例和大小到600x360像素 crop the best fitting 5:3 (600x360) ratio and resize to 600x360 pixel
$img->fit(600, 360);

// 裁剪最合适的1:1比例(200x200),调整为200x200像素 crop the best fitting 1:1 ratio (200x200) and resize to 200x200 pixel
$img->fit(200);

// 添加回调函数以保留最大的原始图像大小 add callback functionality to retain maximal original image size
$img->fit(800, 600, function ($constraint) {
    $constraint->upsize();
});

resize – 调整大小

// create instance
$img = Image::make('public/foo.jpg')

// resize image to fixed size
$img->resize(300, 200);

// resize only the width of the image
$img->resize(300, null);

// resize only the height of the image
$img->resize(null, 200);

// resize the image to a width of 300 and constrain aspect ratio (auto height)
$img->resize(300, null, function ($constraint) {
    $constraint->aspectRatio();
});

// resize the image to a height of 200 and constrain aspect ratio (auto width)
$img->resize(null, 200, function ($constraint) {
    $constraint->aspectRatio();
});

// prevent possible upsizing
$img->resize(null, 400, function ($constraint) {
    $constraint->aspectRatio();
    $constraint->upsize();
});

如何用 Kubernetes 来部署一个 PHP 应用 到 阿里云

根据 原文 结合阿里云改编。

前言 Introduction

Kubernetes是一个开源的容器编制系统。它允许您创建、更新和扩展容器,而无需担心停机。

要运行一个PHP应用程序,Nginx充当PHP- FPM的代理。将此设置装入一个容器可能是一个很麻烦的过程,但是Kubernetes可以帮助在分开的容器中管理这两个服务。使用Kubernetes将允许您保持容器的可重用性和可切换性,并且您不必每次有新版本的Nginx或PHP时都重新构建容器映像。

Step 1 — 创建 PHP-FPM 和 Nginx 服务

在这个步骤,你将创建 PHP-FPM 和 Nginx 服务,在这个集群里,一个服务可以访问一组 pods,集群里的服务可以直接通过他们的 名字 来通讯,不需要 IP 地址。PHP-FPM 服务可以访问 PHP-FPM 的pods,Nginx 服务可以访问 Nginx pods。

由于Nginx pods将代理PHP-FPM pods,您需要告诉服务如何找到它们。您将利用Kubernetes的自动服务发现,使用人类可读的名称将请求路由到服务而不是使用IP地址。

要创建服务,您将创建一个对象定义文件。每个Kubernetes对象定义都是一个YAML文件,其中至少包含以下内容:

  • apiVersion: The version of the Kubernetes API that the definition belongs to.
  • kind: Kubernetes 对象. 例如, a pod or service.
  • metadata: This contains the name of the object along with any labels that you may wish to apply to it.
  • spec: This contains a specific configuration depending on the kind of object you are creating, such as the container image or the ports on which the container will be accessible from.

首先,您将创建一个目录来保存Kubernetes对象定义。这里我们使用 阿里云 托管版Kubernetes。只需创建Worker节点,Master节点由容器服务创建并托管。具备简单、低成本、高可用、无需运维管理Kubernetes集群Master节点的特点,您可以更多关注业务本身。

mkdir definitions && cd definitions

创建 php_service.yaml 文件来定义 PHP-FPM 服务:

nano php_service.yaml

设置 kind 为 Service 

...
apiVersion: v1
kind: Service

给这个服务取名 php, 因为它将提供对PHP-FPM的访问:

...
metadata:
  name: php

您将使用标签对不同的对象进行逻辑分组。使用标签将对象分组到“层”中,如前端或后端。PHP pods将运行在这个服务的后面,因此您将把它标记为tier:backend

...
  labels:
    tier: backend

决定一个服务去访问哪些 pods 是通过 selector 标签。 一个 pod 匹配这些标签将可以通过服务访问,这与pod是在服务之前还是之后创建的无关。您将在后面为pod添加标签。

给这个 pod 打上标签 tier: backend ,归类到 后端层组。你还可以 加上 app: php 标签来标记这个 pod 执行 PHP,将这两个标签添加到 metadata。

...
spec:
  selector:
    app: php
    tier: backend

接下来,给这个服务一个访问端口。这儿使用 9000 端口:添加下面的设置到 spec 下面:

...
  ports:
    - protocol: TCP
      port: 9000

你完整的 php_service.yaml 文件应该像这样:

apiVersion: v1
kind: Service
metadata:
  name: php
  labels:
    tier: backend
spec:
  selector:
    app: php
    tier: backend
  ports:
  - protocol: TCP
    port: 9000

现在你可以用 kubectl apply 创建你的服务,使用 -f 指定要执行的文件

kubectl apply -f php_service.yaml

服务创建成功会返回:

Outputservice/php created

确认你的服务正在执行中:

kubectl get svc

你可以看到 PHP-FPM 的服务在正执行中:

OutputNAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP    10m
php          ClusterIP   10.100.59.238   <none>        9000/TCP   5m

There are various service types that Kubernetes supports. Your php service uses the default service type, ClusterIP. This service type assigns an internal IP and makes the service reachable only from within the cluster.

Now that the PHP-FPM service is ready, you will create the Nginx service. Create and open a new file called nginx_service.yaml with the editor:

nano nginx_service.yaml

This service will target Nginx pods, so you will name it nginx. You will also add a tier: backendlabel as it belongs in the backend tier:nginx_service.yaml

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    tier: backend

Similar to the php service, target the pods with the selector labels app: nginx and tier: backend. Make this service accessible on port 80, the default HTTP port.nginx_service.yaml

...
spec:
  selector:
    app: nginx
    tier: backend
  ports:
  - protocol: TCP
    port: 80

The Nginx service will be publicly accessible to the internet from your Droplet’s public IP address. your_public_ip can be found from your DigitalOcean Cloud Panel. Under spec.externalIPs, add:nginx_service.yaml

...
spec:
  externalIPs:
  - your_public_ip

Your nginx_service.yaml file will look like this:nginx_service.yaml

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    tier: backend
spec:
  selector:
    app: nginx
    tier: backend
  ports:
  - protocol: TCP
    port: 80
  externalIPs:
  - your_public_ip    

Save and close the file. Create the Nginx service:

kubectl apply -f nginx_service.yaml

You will see the following output when the service is running:

Outputservice/nginx created

You can view all running services by executing:

kubectl get svc

You will see both the PHP-FPM and Nginx services listed in the output:

OutputNAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP    13m
nginx        ClusterIP   10.102.160.47   your_public_ip 80/TCP     50s
php          ClusterIP   10.100.59.238   <none>        9000/TCP   8m

Please note, if you want to delete a service you can run:

kubectl delete svc/service_name

Now that you’ve created your PHP-FPM and Nginx services, you will need to specify where to store your application code and configuration files.

Step 2 — 有状态服务-动态云盘使用最佳实践

具体参考阿里云官方文档

你的 code_volume.yaml 文件会像这样:

kind: StorageClass
apiVersion: storage.k8s.io/v1beta1
metadata:
  name: alicloud-disk-ssd-hangzhou-b
provisioner: alicloud/disk
reclaimPolicy: Retain
parameters:
  type: cloud_ssd
  regionid: cn-hangzhou
  zoneid: cn-hangzhou-b
  fstype: "ext4"
  readonly: "false"
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: code
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: alicloud-disk-ssd-hangzhou-b
  resources:
    requests:
      storage: 20Gi

Step 3 — 创建 PHP-FPM 无状态应用

In this step, you will learn how to use a Deployment to create your PHP-FPM pod. Deployments provide a uniform way to create, update, and manage pods by using ReplicaSets. If an update does not work as expected, a Deployment will automatically rollback its pods to a previous image.

The Deployment spec.selector key will list the labels of the pods it will manage. It will also use the template key to create the required pods.

This step will also introduce the use of Init Containers. Init Containers run one or more commands before the regular containers specified under the pod’s template key. In this tutorial, your Init Container will fetch a sample index.php file from GitHub Gist using wget. These are the contents of the sample file:index.php

<?php
echo phpinfo(); 

To create your Deployment, open a new file called php_deployment.yaml with your editor:

创建一个新的文件 `php_deployment.yaml` 来定义 PHP 无状态(Deployment)应用

nano php_deployment.yaml

This Deployment will manage your PHP-FPM pods, so you will name the Deployment object php. The pods belong to the backend tier, so you will group the Deployment into this group by using the tier: backend label:

这个无状态应用管理你的 PHP-FPM 的 Pods,所以它的名字是 php。 pods 属于后太层面,所以你可以使用 tier: backend 给无状态应用一个分组。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: php
  labels:
    tier: backend

For the Deployment spec, you will specify how many copies of this pod to create by using the replicas parameter. The number of replicas will vary depending on your needs and available resources. You will create one replica in this tutorial:

无状态应用的 spec,你可以通过 replicas 定义这个应用需要创建多少个 pods。根据资源的需求给出相应的定义。

...
spec:
  replicas: 1

This Deployment will manage pods that match the app: php and tier: backend labels. Under selector key add:

这个无状态应用会管理有 app:php 和 tier: backend 标签的 pods。定义在 selector 下面:

...
  selector:
    matchLabels:
      app: php
      tier: backend

Next, the Deployment spec requires the template for your pod’s object definition. This template will define specifications to create the pod from. First, you will add the labels that were specified for the php service selectors and the Deployment’s matchLabels. Add app: php and tier: backend under template.metadata.labels:

接下来,无状态应用 的 spec 需要用 template 来定义你的 pod 对象。首先,需要增加一些 php 服务 和 无状态应用 matchLabels 指定的标签。在 template.metadata.labels 下添加 app: php 和 tier: backend

...
  template:
    metadata:
      labels:
        app: php
        tier: backend

A pod can have multiple containers and volumes, but each will need a name. You can selectively mount volumes to a container by specifying a mount path for each volume.

一个 pod 可以拥有多个容器和存储卷,但每个都需要一个名字。通过为每个卷指定挂载路径,可以有选择地将卷挂载到容器。

First, specify the volumes that your containers will access. You created a PVC named code to hold your application code, so name this volume code as well. Under spec.template.spec.volumes, add the following:

首先,指定容器将访问的卷。你可以创建一个 名叫 code 的 PVC 来存储你程序的代码, 使用给这个卷也取名 code,放在 spec.template.spec.volumes 下面:

...
    spec:
      volumes:
      - name: code
        persistentVolumeClaim:
          claimName: code

Next, specify the container you want to run in this pod. You can find various images on the Docker store, but in this tutorial, you will use the php:7-fpm image.

接下来,指定要在这个 pod 中运行的容器。您可以在Docker商店中找到各种镜像,但是在这儿将使用 php:7-fpm 镜像。

在 spec.template.spec.containers 下增加:

...
      containers:
      - name: php
        image: php:7-fpm

Next, you will mount the volumes that the container requires access to. This container will run your PHP code, so it will need access to the code volume. You will also use mountPath to specify /code as the mount point.

再次,你需要挂载这个容器需要访问的卷, 这个容器将要执行 PHP 代码,使用需要访问 code 卷,你需要使用 mountPath 到 /code 来指定挂载点。

在 spec.template.spec.containers.volumeMounts 添加:

...
        volumeMounts:
        - name: code
          mountPath: /code

Now that you have mounted your volume, you need to get your application code on the volume. You may have previously used FTP/SFTP or cloned the code over an SSH connection to accomplish this, but this step will show you how to copy the code using an Init Container.

现在你已经挂载了存储卷,你需要吧你的代码放到存储卷里,你可以提前用 FTP/SFTP 或者 SSH 实现。这儿为直接使用 kubectl cp 来复制代码。

# Copy /tmp/foo_dir local directory to /tmp/bar_dir in a remote pod in the default namespace
# 复制本地 ./src 目录到远程默认命名空间的 PHP pod 的 /code 目录
kubectl cp ./src <php-pod>:/code

最终,你的 php_deployment.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: php
  labels:
    tier: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: php
      tier: backend
  template:
    metadata:
      labels:
        app: php
        tier: backend
    spec:
      volumes:
      - name: code
        persistentVolumeClaim:
          claimName: code
      containers:
      - name: php
        image: php:7-fpm
        volumeMounts:
        - name: code
          mountPath: /code

Save the file and exit the editor.

Create the PHP-FPM Deployment with kubectl:

kubectl apply -f php_deployment.yaml

You will see the following output upon Deployment creation:

Outputdeployment.apps/php created

To summarize, this Deployment will start by downloading the specified images. It will then request the PersistentVolume from your PersistentVolumeClaim and serially run your initContainers. Once complete, the containers will run and mount the volumes to the specified mount point. Once all of these steps are complete, your pod will be up and running.

You can view your Deployment by running:

kubectl get deployments

You will see the output:

OutputNAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
php       1         1         1            0           19s

This output can help you understand the current state of the Deployment. A Deployment is one of the controllers that maintains a desired state. The template you created specifies that the DESIRED state will have 1 replicas of the pod named php. The CURRENT field indicates how many replicas are running, so this should match the DESIRED state. You can read about the remaining fields in the Kubernetes Deployments documentation.

You can view the pods that this Deployment started with the following command:

kubectl get pods

The output of this command varies depending on how much time has passed since creating the Deployment. If you run it shortly after creation, the output will likely look like this:

OutputNAME                   READY     STATUS     RESTARTS   AGE
php-86d59fd666-bf8zd   0/1       Init:0/1   0          9s

The columns represent the following information:

  • Ready: The number of replicas running this pod.
  • Status: The status of the pod. Init indicates that the Init Containers are running. In this output, 0 out of 1 Init Containers have finished running.
  • Restarts: How many times this process has restarted to start the pod. This number will increase if any of your Init Containers fail. The Deployment will restart it until it reaches a desired state.

Depending on the complexity of your startup scripts, it can take a couple of minutes for the status to change to podInitializing:

OutputNAME                   READY     STATUS            RESTARTS   AGE
php-86d59fd666-lkwgn   0/1       podInitializing   0          39s

This means the Init Containers have finished and the containers are initializing. If you run the command when all of the containers are running, you will see the pod status change to Running.

OutputNAME                   READY     STATUS            RESTARTS   AGE
php-86d59fd666-lkwgn   1/1       Running   0          1m

You now see that your pod is running successfully. If your pod doesn’t start, you can debug with the following commands:

  • View detailed information of a pod:
kubectl describe pods pod-name
  • View logs generated by a pod:
kubectl logs pod-name
  • View logs for a specific container in a pod:
kubectl logs pod-name container-name

Your application code is mounted and the PHP-FPM service is now ready to handle connections. You can now create your Nginx Deployment.

Step 4 — 创建 Nginx 无状态应用

In this step, you will use a ConfigMap to configure Nginx. A ConfigMap holds your configuration in a key-value format that you can reference in other Kubernetes object definitions. This approach will grant you the flexibility to reuse or swap the image with a different Nginx version if needed. Updating the ConfigMap will automatically replicate the changes to any pod mounting it.

Create a nginx_configMap.yaml file for your ConfigMap with your editor:

nano nginx_configMap.yaml

Name the ConfigMap nginx-config and group it into the tier: backend micro-service:nginx_configMap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
  labels:
    tier: backend

Next, you will add the data for the ConfigMap. Name the key config and add the contents of your Nginx configuration file as the value. You can use the example Nginx configuration from this tutorial.

Because Kubernetes can route requests to the appropriate host for a service, you can enter the name of your PHP-FPM service in the fastcgi_pass parameter instead of its IP address. Add the following to your nginx_configMap.yaml file:nginx_configMap.yaml

...
data:
  config : |
    server {
      index index.php index.html;
      error_log  /var/log/nginx/error.log;
      access_log /var/log/nginx/access.log;
      root ^/code^;

      location / {
          try_files $uri $uri/ /index.php?$query_string;
      }

      location ~ \.php$ {
          try_files $uri =404;
          fastcgi_split_path_info ^(.+\.php)(/.+)$;
          fastcgi_pass php:9000;
          fastcgi_index index.php;
          include fastcgi_params;
          fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
          fastcgi_param PATH_INFO $fastcgi_path_info;
        }
    }

Your nginx_configMap.yaml file will look like this:nginx_configMap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
  labels:
    tier: backend
data:
  config : |
    server {
      index index.php index.html;
      error_log  /var/log/nginx/error.log;
      access_log /var/log/nginx/access.log;
      root /code;

      location / {
          try_files $uri $uri/ /index.php?$query_string;
      }

      location ~ \.php$ {
          try_files $uri =404;
          fastcgi_split_path_info ^(.+\.php)(/.+)$;
          fastcgi_pass php:9000;
          fastcgi_index index.php;
          include fastcgi_params;
          fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
          fastcgi_param PATH_INFO $fastcgi_path_info;
        }
    }

Save the file and exit the editor.

Create the ConfigMap:

kubectl apply -f nginx_configMap.yaml

You will see the following output:

Outputconfigmap/nginx-config created

You’ve finished creating your ConfigMap and can now build your Nginx Deployment.

Start by opening a new nginx_deployment.yaml file in the editor:

nano nginx_deployment.yaml

Name the Deployment nginx and add the label tier: backend:nginx_deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    tier: backend

Specify that you want one replicas in the Deployment spec. This Deployment will manage pods with labels app: nginx and tier: backend. Add the following parameters and values:nginx_deployment.yaml

...
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
      tier: backend

Next, add the pod template. You need to use the same labels that you added for the Deployment selector.matchLabels. Add the following:nginx_deployment.yaml

...
  template:
    metadata:
      labels:
        app: nginx
        tier: backend

Give Nginx access to the code PVC that you created earlier. Under spec.template.spec.volumes, add:nginx_deployment.yaml

...
    spec:
      volumes:
      - name: code
        persistentVolumeClaim:
          claimName: code

Pods can mount a ConfigMap as a volume. Specifying a file name and key will create a file with its value as the content. To use the ConfigMap, set path to name of the file that will hold the contents of the key. You want to create a file site.conf from the key config. Under spec.template.spec.volumes, add the following:nginx_deployment.yaml

...
      - name: config
        configMap:
          name: nginx-config
          items:
          - key: config
            path: site.conf

Warning: If a file is not specified, the contents of the key will replace the mountPath of the volume. This means that if a path is not explicitly specified, you will lose all content in the destination folder.

Next, you will specify the image to create your pod from. This tutorial will use the nginx:1.7.9image for stability, but you can find other Nginx images on the Docker store. Also, make Nginx available on the port 80. Under spec.template.spec add:nginx_deployment.yaml

...
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

Nginx and PHP-FPM need to access the file at the same path, so mount the code volume at /code:nginx_deployment.yaml

...
        volumeMounts:
        - name: code
          mountPath: /code

The nginx:1.7.9 image will automatically load any configuration files under the /etc/nginx/conf.d directory. Mounting the config volume in this directory will create the file /etc/nginx/conf.d/site.conf. Under volumeMounts add the following:nginx_deployment.yaml

...
        - name: config
          mountPath: /etc/nginx/conf.d

Your nginx_deployment.yaml file will look like this:nginx_deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    tier: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
      tier: backend
  template:
    metadata:
      labels:
        app: nginx
        tier: backend
    spec:
      volumes:
      - name: code
        persistentVolumeClaim:
          claimName: code
      - name: config
        configMap:
          name: nginx-config
          items:
          - key: config
            path: site.conf
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
        volumeMounts:
        - name: code
          mountPath: /code
        - name: config
          mountPath: /etc/nginx/conf.d

Save the file and exit the editor.

Create the Nginx Deployment:

kubectl apply -f nginx_deployment.yaml

The following output indicates that your Deployment is now created:

Outputdeployment.apps/nginx created

List your Deployments with this command:

kubectl get deployments

You will see the Nginx and PHP-FPM Deployments:

OutputNAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx     1         1         1            0           16s
php       1         1         1            1           7m

List the pods managed by both of the Deployments:

kubectl get pods

You will see the pods that are running:

OutputNAME                     READY     STATUS    RESTARTS   AGE
nginx-7bf5476b6f-zppml   1/1       Running   0          32s
php-86d59fd666-lkwgn     1/1       Running   0          7m

Now that all of the Kubernetes objects are active, you can visit the Nginx service on your browser.

List the running services:

kubectl get services -o wide

Get the External IP for your Nginx service:

OutputNAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE       SELECTOR
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP    39m       <none>
nginx        ClusterIP   10.102.160.47   your_public_ip 80/TCP     27m       app=nginx,tier=backend
php          ClusterIP   10.100.59.238   <none>        9000/TCP   34m       app=php,tier=backend

On your browser, visit your server by typing in http://your_public_ip. You will see the output of php_info() and have confirmed that your Kubernetes services are up and running.

总结

In this guide, you containerized the PHP-FPM and Nginx services so that you can manage them independently. This approach will not only improve the scalability of your project as you grow, but will also allow you to efficiently use resources as well. You also stored your application code on a volume so that you can easily update your services in the future.

重启php-fpm如果进程意外死掉

#!/bin/sh
# */5 * * * * /var/www/php.sh >>  /var/www/php.log

php_fpm_count=$(ps aux | grep php-fpm | wc -l)
echo "php_fpm_count is $php_fpm_count"

if [ $php_fpm_count -lt 2 ]; then
    echo "Sorry, we have a problem with php-fpm! restart it"
    service php7.2-fpm restart
    chmod -R 777 /run/php # 如果使用sock,确保sock有执行权限
fi

*/5 * * * * /var/www/php.sh >> /var/www/php.log

放到 任务计划里,每5分钟执行一次。