Magento 2에 캐시 레이어 분석 및 시도 추가

29237 단어
검은 금요일은 놀랍게도 지나갔지만 마젠토 2 커뮤니티 버전은 읽기와 쓰기가 분리되지 않아 사이트 전체에 걸린 날카로운 칼이었다.
나는 이전에 Magento 2에 MySQL 읽기와 쓰기 분리 플러그인을 써 보았는데 Magento 2의 데이터베이스 접근층을 깊이 연구한 후에 간단한 플러그인을 통해 읽기와 쓰기를 분리하는 것은 기본적으로 불가능하다는 것을 발견했다.Magento 2 커뮤니티 버전 읽기와 쓰기 데이터베이스의 논리에는 대량의 Magento 1의 코드와 논리가 섞여 있어 소량의 코드를 수정하는 전제에서 읽기와 쓰기를 분리할 수 없었다. 나중에 사이트의 각종 수요를 만들느라 바빠서 읽기와 쓰기 분리는 보류되었다.
이번 흑오에서 전체 프로젝트의 성능 병목은 바로 MySQL이다. 데이터가 올라온 후에 응용 서버의 부하는 기본적으로 변하지 않았지만 데이터베이스 서버의 부하는 3배가 넘었고 데이터베이스 서버가 하드웨어 설정을 앞당겨 업그레이드한 토대에서 나타난다.그래서 Magento 2의 데이터베이스 층은 반드시 최적화해야 한다고 생각합니다. 읽기와 쓰기 분리를 할 수 없으니 캐시 층을 추가할 수 있을까요?절대 다수의 읽기 조작을 캐시층으로 옮기면 이론적으로 데이터베이스의 부하가 상응하여 떨어진다.
코드를 가장 적게 고치려면 적당한 곳을 찾아야 한다.Magento 2의 데이터베이스 어댑터는 Magento\Framework\DB\Adapter\Pdo\Mysql 클래스이며, 이 클래스는 Zend 에서 상속됩니다.Db_Adapter_Abstract
데이터를 가져오는 모든 방법은 다음과 같습니다.
Zend_Db_Adapter_Abstract::fetchAll($sql, $bind = array(), $fetchMode = null)

Zend_Db_Adapter_Abstract::fetchAssoc($sql, $bind = array())

Zend_Db_Adapter_Abstract::fetchCol($sql, $bind = array())

Zend_Db_Adapter_Abstract::fetchPairs($sql, $bind = array())

Zend_Db_Adapter_Abstract::fetchOne($sql, $bind = array())

Zend_Db_Adapter_Abstract::fetchRow($sql, $bind = array(), $fetchMode = null)

이 가운데 fetchAll()과 fetchRow()는 가장 많이 쓰이는 두 개다.
다음은fetchRow()의 예를 들어 이 방안의 타당성과 실현 방법을 분석한다.
/**
 * Fetches the first row of the SQL result.
 * Uses the current fetchMode for the adapter.
 *
 * @param string|Zend_Db_Select $sql An SQL SELECT statement.
 * @param mixed $bind Data to bind into SELECT placeholders.
 * @param mixed                 $fetchMode Override current fetch mode.
 * @return mixed Array, object, or scalar depending on fetch mode.
 */
public function fetchRow($sql, $bind = array(), $fetchMode = null)

$sql 대상과 $bind 그룹을 분석하면 1을 포함하는 정확한 포맷된 데이터를 얻을 수 있습니다.데이터베이스 테이블 이름 2.필드 키 값 쌍
이 데이터를 통해 캐시 키(key)와 탭(tag)을 구축할 수 있습니다. 예를 들어 $cacheKey = tablename::주 키 값이 맞거나 $cacheKey = tablename::유일한 키 인덱스 키 값 쌍
$cacheTags = [table name, table name: 주 키 값 대 table name: 유일한 키 인덱스 키 값 대 그룹 1, table name::유일한 키 인덱스 키 값 대 그룹 2,...]
cacheTags의 역할은 캐시를 분류하여 후속 정리를 편리하게 하는 것이다.
$cacheKey, $cacheTags가 있으면 데이터베이스 조회 결과를 캐시에 저장할 수 있습니다.
다음에 다시 조회를 하면 캐시에서 대응하는 데이터가 있는지 확인하고 있으면 데이터 호출자에게 직접 되돌려줍니다.
그러면 데이터가 업데이트되면요?
데이터 업데이트는 세 가지로 나뉜다.UPDATE, 2. INSERT, 3 DELETE
UPDATE의 경우:
/**
 * Updates table rows with specified data based on a WHERE clause.
 *
 * @param  mixed        $table The table to update.
 * @param  array        $bind  Column-value pairs.
 * @param  mixed        $where UPDATE WHERE clause(s).
 * @return int          The number of affected rows.
 * @throws Zend_Db_Adapter_Exception
 */
public function update($table, array $bind, $where = '')

업데이트 () 방법은 3개의 매개 변수를 수신하는데, 각각tablename, 업데이트할 데이터 키 값이 맞습니다.where 조건 서브문장입니다.방금 $cacheTags를 구축할 때 각각tablename、table_name::메인 키 값이 맞고tablename::유일한 키 인덱스 키 값 쌍,tablename는 이미 만들어진 것입니다. 나머지 두 가지 tag는where 자구에서 해석해야 합니다.해석을 통해 최악의 경우where 자구가 키 값 쌍을 해석하지 못했고, 가장 좋은 경우는 모든 filed 키 값 쌍을 해석한 것이다.최악의 경우 테이블을 제거해야 합니다name의 모든 캐시 데이터는 캐시 데이터 하나만 지우면 됩니다.
INSERT의 경우:
/**
 * Inserts a table row with specified data.
 *
 * @param mixed $table The table to insert data into.
 * @param array $bind Column-value pairs.
 * @return int The number of affected rows.
 * @throws Zend_Db_Adapter_Exception
 */
public function insert($table, array $bind)

insert () 방법은 두 개의 매개 변수를 수신하는데, 각각tablename, 삽입할 데이터 키 값이 맞습니다.새로 삽입된 데이터가 캐시에 존재하지 않기 때문에 캐시를 조작할 필요가 없습니다
DELETE의 경우:
/**
 * Deletes table rows based on a WHERE clause.
 *
 * @param  mixed        $table The table to update.
 * @param  mixed        $where DELETE WHERE clause(s).
 * @return int          The number of affected rows.
 */
public function delete($table, $where = '')

delete () 방법으로 2개의 매개 변수를 수신,tablewhere 자구와where 자구에서 메인 키 값 쌍이나 유일한 키 인덱스 키 값 쌍을 해석할 수 있다면 캐시 기록을 지우기만 하면 됩니다. 그렇지 않으면 이tablename 아래의 모든 캐시 레코드
최적화 효과: 저는 잠시 ab로 Magento 2의 카트를 테스트했습니다.
ab -C PHPSESSID=acmsj8q8ld1tvdo77lm5t0dr9b -n 40 -c 5  http://localhost/checkout/cart/

캐시가 없을 때:test-No-cache-1:
Requests per second:    1.79 [#/sec] (mean)
Time per request:       2786.478 [ms] (mean)
Time per request:       557.296 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%    756
  66%   2064
  75%   5635
  80%   6150
  90%   7632
  95%   8530
  98%   8563
  99%   8563
 100%   8563 (longest request)
 
MySQL     CPU        20% ~ 24%

test-No-Cache-2:
Requests per second:    1.84 [#/sec] (mean)
Time per request:       2720.852 [ms] (mean)
Time per request:       544.170 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%    586
  66%   1523
  75%   4036
  80%   5667
  90%  10228
  95%  11621
  98%  12098
  99%  12098
 100%  12098 (longest request)
 
MySQL     CPU        20% ~ 24%

캐시가 있을 때:test-With-cache-1:
Requests per second:    1.99 [#/sec] (mean)
Time per request:       2509.273 [ms] (mean)
Time per request:       501.854 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%    489
  66%    511
  75%    574
  80%    637
  90%  19073
  95%  19553
  98%  20063
  99%  20063
 100%  20063 (longest request)
 
MySQL     CPU        5%   

test-With-Cache-2:
Requests per second:    2.10 [#/sec] (mean)
Time per request:       2384.145 [ms] (mean)
Time per request:       476.829 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%    465
  66%    472
  75%    565
  80%    620
  90%   9509
  95%  18374
  98%  18588
  99%  18588
 100%  18588 (longest request)

MySQL     CPU        5% ~ 7 %

상기 두 그룹의 데이터를 비교해 보면 MySQL의 CPU 점용률이 큰 폭으로 떨어졌다(20%에서 5%로 떨어졌다). 이를 통해 알 수 있듯이 캐시층을 늘리는 것이 MySQL 부하를 낮추는 데 효과가 있다.
그러나 작은 문제가 하나 있다. 캐시를 사용하지 않는 상황에서Percentage of the requests served within a certain time라는 값은 90% 지점 이후에 캐시가 있는 것보다 잘 나타난다. 대량의 unserialize () 조작으로 인해 CPU 자원이 부족하여 응답이 느린 것으로 추정된다.
수정된 vendor/magento/framework/DB/Adapter/pdo/Mysql.php:
class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface
{

    protected $_cache;


    public function fetchAll($sql, $bind = array(), $fetchMode = null)
    {
        if ($sql instanceof \Zend_Db_Select) {
            /** @var array $from */
            $from = $sql->getPart('from');
            $tableName = current($from)['tableName'];
            $cacheKey = 'FETCH_ALL::' . $tableName . '::' . md5((string)$sql);
            $cache = $this->getCache();
            $data = $cache->load($cacheKey);
            if ($data === false) {
                $data = parent::fetchAll($sql, $bind, $fetchMode);
                $cache->save(serialize($data), $cacheKey, ['FETCH_ALL::' . $tableName], 3600);
            } else {
                $data = @unserialize($data);
            }
        } else {
            $data = parent::fetchAll($sql, $bind, $fetchMode);
        }
        return $data;
    }

    public function fetchRow($sql, $bind = [], $fetchMode = null)
    {
        $cacheIdentifiers = $this->resolveSql($sql, $bind);
        if ($cacheIdentifiers !== false) {
            $cache = $this->getCache()->getFrontend();
            $data = $cache->load($cacheIdentifiers['cacheKey']);

            if ($data === false) {
                $data = parent::fetchRow($sql, $bind, $fetchMode);
                if ($data) {
                    $cache->save(serialize($data), $cacheIdentifiers['cacheKey'], $cacheIdentifiers['cacheTags'], 3600);
                }
            } else {
                $data = @unserialize($data);
            }
        } else {
            $data = parent::fetchRow($sql, $bind, $fetchMode);
        }
        return $data;
    }

    public function update($table, array $bind, $where = '')
    {
        parent::update($table, $bind, $where);
        $cacheKey = $this->resolveUpdate($table, $bind, $where);
        if ($cacheKey === false) {
            $cacheKey = $table;
        }
        $this->getCache()->clean([$cacheKey, 'FETCH_ALL::' . $table]);
    }

    /**
     * @return \Magento\Framework\App\CacheInterface
     */
    private function getCache()
    {
        if ($this->_cache === null) {
            $objectManager = \Magento\Framework\App\ObjectManager::getInstance();
            $this->_cache = $objectManager->get(\Magento\Framework\App\CacheInterface::class);
        }
        return $this->_cache;
    }

    /**
     * @param string|\Zend_Db_Select $sql An SQL SELECT statement.
     * @param mixed $bind Data to bind into SELECT placeholders.
     * @return array
     */
    protected function resolveSql($sql, $bind = array())
    {
        $result = false;
        if ($sql instanceof \Zend_Db_Select) {
            try {
                /** @var array $from */
                $from = $sql->getPart('from');
                $tableName = current($from)['tableName'];
                $where = $sql->getPart('where');

                foreach ($this->getIndexFields($tableName) as $indexFields) {
                    $kv = $this->getKv($indexFields, $where, $bind);
                    if ($kv !== false) {
                        $cacheKey = $tableName . '::' . implode('|', $kv);
                        $cacheTags = [
                            $tableName,
                            $cacheKey
                        ];
                        $result = ['cacheKey' => $cacheKey, 'cacheTags' => $cacheTags];
                    }
                }
            }catch (\Zend_Db_Select_Exception $e) {

            }
        }
        return $result;
    }

    protected function resolveUpdate($tableName, array $bind, $where = '')
    {
        $cacheKey = false;
        if (is_string($where)) {
            $where = [$where];
        }
        foreach ($this->getIndexFields($tableName) as $indexFields) {
            $kv = $this->getKv($indexFields, $where, $bind);
            if ($kv !== false) {
                $cacheKey = $tableName . '::' . implode('|', $kv);
            }
        }
        return $cacheKey;
    }

    protected function getIndexFields($tableName)
    {
        $indexes = $this->getIndexList($tableName);

        $indexFields = [];
        foreach ($indexes as $data) {
            if ($data['INDEX_TYPE'] == 'primary') {
                $indexFields[] = $data['COLUMNS_LIST'];
            } elseif ($data['INDEX_TYPE'] == 'unique') {
                $indexFields[] = $data['COLUMNS_LIST'];
            }
        }
        return $indexFields;
    }

    protected function getKv($fields, $where, $bind)
    {
        $found = true;
        $kv = [];
        foreach ($fields as $field) {
            $_found = false;

            if (isset($bind[':' . $field])) {   //   bind       filed value
                $kv[$field] = $field . '=' .$bind[':' . $field];
                $_found = true;
            } elseif (is_array($where)) {
                foreach ($where as $case) { //    where     ,    filed value
                    $matches = [];
                    $preg = sprintf('#%s.*=(.*)#', $field);
                    $_result = preg_match($preg, $case, $matches);
                    if ($_result) {
                        $kv[$field] = $field . '=' .trim($matches[1], ' \')');
                        $_found = true;
                    }
                }
            }

            if (!$_found) { //      field    ,
                $found = false;
                break;
            }
        }
        return $found ? $kv : false;
    }
}

 
전재 대상:https://www.cnblogs.com/jpdoutop/p/10026592.html

좋은 웹페이지 즐겨찾기