Magento 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
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
다양한 언어의 JSONJSON은 Javascript 표기법을 사용하여 데이터 구조를 레이아웃하는 데이터 형식입니다. 그러나 Javascript가 코드에서 이러한 구조를 나타낼 수 있는 유일한 언어는 아닙니다. 저는 일반적으로 '객체'{}...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.